f24d138ab4
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>
214 lines
7.3 KiB
Python
214 lines
7.3 KiB
Python
"""Rich console singleton, theme, banner, and all display helpers."""
|
||
|
||
from __future__ import annotations
|
||
|
||
from dataclasses import dataclass
|
||
from pathlib import Path
|
||
from typing import Optional
|
||
|
||
import rich.box
|
||
from rich.align import Align
|
||
from rich.columns import Columns
|
||
from rich.console import Console
|
||
from rich.panel import Panel
|
||
from rich.rule import Rule
|
||
from rich.syntax import Syntax
|
||
from rich.table import Table
|
||
from rich.text import Text
|
||
from rich.theme import Theme
|
||
|
||
FREEPIK_THEME = Theme(
|
||
{
|
||
"info": "bold cyan",
|
||
"success": "bold green",
|
||
"warning": "bold yellow",
|
||
"error": "bold red",
|
||
"model": "bold magenta",
|
||
"taskid": "bold blue",
|
||
"path": "bold white underline",
|
||
"dim.label": "dim white",
|
||
"highlight": "bold magenta",
|
||
"brand": "bold magenta",
|
||
}
|
||
)
|
||
|
||
console = Console(theme=FREEPIK_THEME, highlight=True)
|
||
err_console = Console(stderr=True, theme=FREEPIK_THEME)
|
||
|
||
BANNER = """\
|
||
[bold magenta] ███████╗██████╗ ███████╗███████╗██████╗ ██╗██╗ ██╗[/bold magenta]
|
||
[bold magenta] ██╔════╝██╔══██╗██╔════╝██╔════╝██╔══██╗██║██║ ██╔╝[/bold magenta]
|
||
[bold magenta] █████╗ ██████╔╝█████╗ █████╗ ██████╔╝██║█████╔╝ [/bold magenta]
|
||
[bold magenta] ██╔══╝ ██╔══██╗██╔══╝ ██╔══╝ ██╔═══╝ ██║██╔═██╗ [/bold magenta]
|
||
[bold magenta] ██║ ██║ ██║███████╗███████╗██║ ██║██║ ██╗[/bold magenta]
|
||
[bold magenta] ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝[/bold magenta]
|
||
[dim] AI Media Generation CLI • v0.1.0[/dim]"""
|
||
|
||
|
||
def print_banner() -> None:
|
||
console.print()
|
||
console.print(Align.center(Text.from_markup(BANNER)))
|
||
console.print(Rule(style="magenta dim"))
|
||
console.print()
|
||
|
||
|
||
@dataclass
|
||
class GenerationResult:
|
||
task_id: str
|
||
model: str
|
||
output_path: Path
|
||
width: Optional[int] = None
|
||
height: Optional[int] = None
|
||
seed: Optional[int] = None
|
||
duration: Optional[str] = None
|
||
task_type: str = "image"
|
||
|
||
|
||
def print_result(result: GenerationResult) -> None:
|
||
table = Table(
|
||
show_header=False,
|
||
box=rich.box.SIMPLE,
|
||
padding=(0, 1),
|
||
show_edge=False,
|
||
)
|
||
table.add_column(style="dim.label", width=16, no_wrap=True)
|
||
table.add_column(style="bold", overflow="fold")
|
||
|
||
table.add_row("Model", f"[model]{result.model}[/model]")
|
||
table.add_row("Task ID", f"[taskid]{result.task_id[:16]}…[/taskid]")
|
||
|
||
if result.width and result.height:
|
||
table.add_row("Dimensions", f"{result.width} × {result.height} px")
|
||
if result.duration:
|
||
table.add_row("Duration", f"{result.duration}s")
|
||
if result.seed is not None:
|
||
table.add_row("Seed", str(result.seed))
|
||
else:
|
||
table.add_row("Seed", "[dim]random[/dim]")
|
||
|
||
table.add_row("Saved to", f"[path]{result.output_path}[/path]")
|
||
|
||
title_map = {
|
||
"image": "[success] Image Generated [/success]",
|
||
"video": "[success] Video Generated [/success]",
|
||
"upscale-image": "[success] Image Upscaled [/success]",
|
||
"upscale-video": "[success] Video Upscaled [/success]",
|
||
"icon": "[success] Icon Generated [/success]",
|
||
"expand": "[success] Image Expanded [/success]",
|
||
"describe": "[success] Image Described [/success]",
|
||
}
|
||
title = title_map.get(result.task_type, "[success] Done [/success]")
|
||
|
||
console.print(Panel(table, title=title, border_style="green", padding=(1, 2)))
|
||
|
||
|
||
def print_describe_result(task_id: str, prompt_text: str, output_path: Optional[Path] = None) -> None:
|
||
content = Text(prompt_text, style="italic")
|
||
footer = ""
|
||
if output_path:
|
||
footer = f"\n\n[dim]Saved to:[/dim] [path]{output_path}[/path]"
|
||
|
||
console.print(
|
||
Panel(
|
||
Text.from_markup(f"{prompt_text}{footer}"),
|
||
title="[success] Image Description [/success]",
|
||
subtitle=f"[dim]Task: {task_id[:16]}…[/dim]",
|
||
border_style="green",
|
||
padding=(1, 2),
|
||
)
|
||
)
|
||
|
||
|
||
def print_no_wait(task_id: str, task_type: str, model: str) -> None:
|
||
console.print(
|
||
Panel(
|
||
Text.from_markup(
|
||
f"[info]Task submitted successfully.[/info]\n\n"
|
||
f"[dim.label]Task ID:[/dim.label] [taskid]{task_id}[/taskid]\n"
|
||
f"[dim.label]Type:[/dim.label] {task_type}\n"
|
||
f"[dim.label]Model:[/dim.label] [model]{model}[/model]\n\n"
|
||
f"[dim]The task is processing asynchronously. Results will be available\n"
|
||
f"on the Freepik dashboard when complete.[/dim]"
|
||
),
|
||
title="⏳ Task Queued",
|
||
border_style="cyan",
|
||
padding=(1, 2),
|
||
)
|
||
)
|
||
|
||
|
||
def print_error(message: str, hint: Optional[str] = None) -> None:
|
||
body = f"[error]{message}[/error]"
|
||
if hint:
|
||
body += f"\n\n[dim]Hint:[/dim] {hint}"
|
||
err_console.print(
|
||
Panel(
|
||
Text.from_markup(body),
|
||
title="[error] Error [/error]",
|
||
border_style="red",
|
||
padding=(1, 2),
|
||
)
|
||
)
|
||
|
||
|
||
def print_warning(message: str) -> None:
|
||
console.print(
|
||
Panel(
|
||
Text.from_markup(f"[warning]{message}[/warning]"),
|
||
title="[warning] Warning [/warning]",
|
||
border_style="yellow",
|
||
padding=(0, 2),
|
||
)
|
||
)
|
||
|
||
|
||
def print_config_table(config_dict: dict, masked_keys: set[str] | None = None) -> None:
|
||
masked_keys = masked_keys or {"api_key"}
|
||
table = Table(
|
||
title="[brand]Freepik CLI Configuration[/brand]",
|
||
show_header=True,
|
||
header_style="bold magenta",
|
||
border_style="magenta",
|
||
box=rich.box.ROUNDED,
|
||
padding=(0, 1),
|
||
)
|
||
table.add_column("Key", style="dim.label", width=28)
|
||
table.add_column("Value", style="bold", overflow="fold")
|
||
|
||
for key, value in config_dict.items():
|
||
if key in masked_keys and value:
|
||
display = f"[dim]{'*' * 8}{str(value)[-4:]}[/dim]" if len(str(value)) > 4 else "[dim]****[/dim]"
|
||
elif value is None or value == "":
|
||
display = "[dim]not set[/dim]"
|
||
else:
|
||
display = str(value)
|
||
table.add_row(key, display)
|
||
|
||
console.print(table)
|
||
|
||
|
||
def print_config_toml(config_dict: dict, masked_keys: set[str] | None = None) -> None:
|
||
masked_keys = masked_keys or {"api_key"}
|
||
lines = []
|
||
for key, value in config_dict.items():
|
||
if key in masked_keys:
|
||
continue
|
||
if value is None:
|
||
continue
|
||
if isinstance(value, str):
|
||
lines.append(f'{key} = "{value}"')
|
||
elif isinstance(value, bool):
|
||
lines.append(f"{key} = {str(value).lower()}")
|
||
else:
|
||
lines.append(f"{key} = {value}")
|
||
|
||
toml_str = "\n".join(lines)
|
||
console.print(
|
||
Panel(
|
||
Syntax(toml_str, "toml", theme="monokai", background_color="default"),
|
||
title="[brand]~/.config/freepik-cli/config.toml[/brand]",
|
||
border_style="magenta",
|
||
padding=(1, 2),
|
||
)
|
||
)
|