Files
valknar 941fd14ccf refactor: rename project from Freepik to Magnific
Rename all identifiers, strings, file names, env vars, CLI entry point,
ASCII banner, and API endpoint to reflect the company rebrand.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 13:06:16 +02:00

216 lines
9.3 KiB
Python
Raw Permalink 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 magnific_cli.api.client import MagnificAPIError, MagnificClient
from magnific_cli.api.edit import EditAPI
from magnific_cli.utils.config import MagnificConfig
from magnific_cli.utils.console import GenerationResult, console, print_error, print_no_wait, print_result
from magnific_cli.utils.files import auto_output_path, get_image_dimensions, image_to_base64, save_from_url
from magnific_cli.utils.polling import MagnificTaskError, MagnificTimeoutError, PollConfig, poll_task
_EXPAND_MODELS = ["flux-pro", "ideogram", "seedream-v4-5"]
def _get_api_key(api_key: Optional[str], config: MagnificConfig) -> str:
key = api_key or config.api_key
if not key:
print_error("No API key found.", hint="Set [cyan]MAGNIFIC_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="MAGNIFIC_API_KEY")] = None,
) -> None:
"""
[bold]Expand an image[/bold] by adding new content around its edges (outpainting).
[dim]Examples:[/dim]
magnific expand-image photo.jpg --left 512 --right 512 --prompt "lush forest"
magnific 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 = MagnificConfig.load()
key = _get_api_key(api_key, config)
image_b64 = image_to_base64(image)
with MagnificClient(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 MagnificAPIError 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 magnific_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 (MagnificTaskError, MagnificTimeoutError) as exc:
print_error(str(exc))
raise typer.Exit(1)
from magnific_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="MAGNIFIC_API_KEY")] = None,
) -> None:
"""
[bold]Relight an image[/bold] using AI-controlled lighting. [dim](Premium feature)[/dim]
[dim]Examples:[/dim]
magnific relight portrait.jpg --prompt "dramatic studio lighting"
magnific relight scene.png --prompt "warm golden hour" --output relit.jpg
"""
config = MagnificConfig.load()
key = _get_api_key(api_key, config)
image_b64 = image_to_base64(image)
with MagnificClient(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 MagnificAPIError 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 (MagnificTaskError, MagnificTimeoutError) 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="MAGNIFIC_API_KEY")] = None,
) -> None:
"""
[bold]Apply an artistic style[/bold] from one image onto another. [dim](Premium feature)[/dim]
[dim]Examples:[/dim]
magnific style-transfer photo.jpg painting.jpg --strength 0.8
magnific style-transfer portrait.png van_gogh.jpg --output styled.jpg
"""
config = MagnificConfig.load()
key = _get_api_key(api_key, config)
content_b64 = image_to_base64(content)
style_b64 = image_to_base64(style_image)
with MagnificClient(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 MagnificAPIError 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 (MagnificTaskError, MagnificTimeoutError) 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"))