Files
antigravity-skills-reference/skills/stability-ai/scripts/generate.py
ProgramadorBrasil 61ec71c5c7 feat: add 52 specialized AI agent skills (#217)
New skills covering 10 categories:

**Security & Audit**: 007 (STRIDE/PASTA/OWASP), cred-omega (secrets management)
**AI Personas**: Karpathy, Hinton, Sutskever, LeCun (4 sub-skills), Altman, Musk, Gates, Jobs, Buffett
**Multi-agent Orchestration**: agent-orchestrator, task-intelligence, multi-advisor
**Code Analysis**: matematico-tao (Terence Tao-inspired mathematical code analysis)
**Social & Messaging**: Instagram Graph API, Telegram Bot, WhatsApp Cloud API, social-orchestrator
**Image Generation**: AI Studio (Gemini), Stability AI, ComfyUI Gateway, image-studio router
**Brazilian Domain**: 6 auction specialist modules, 2 legal advisors, auctioneers data scraper
**Product & Growth**: design, invention, monetization, analytics, growth engine
**DevOps & LLM Ops**: Docker/CI-CD/AWS, RAG/embeddings/fine-tuning
**Skill Governance**: installer, sentinel auditor, context management

Each skill includes:
- Standardized YAML frontmatter (name, description, risk, source, tags, tools)
- Structured sections (Overview, When to Use, How it Works, Best Practices)
- Python scripts and reference documentation where applicable
- Cross-platform compatibility (Claude Code, Antigravity, Cursor, Gemini CLI, Codex CLI)

Co-authored-by: ProgramadorBrasil <214873561+ProgramadorBrasil@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 10:04:07 +01:00

688 lines
25 KiB
Python

"""
Script principal de geracao de imagens via Stability AI API.
Suporta: text-to-image (SD3.5, Ultra, Core), img2img, upscale, inpaint,
remove-background, search-and-replace, erase.
Uso:
python generate.py --prompt "a mountain sunset" --mode generate
python generate.py --prompt "watercolor style" --mode img2img --image foto.jpg
python generate.py --mode upscale --image foto.jpg
python generate.py --mode remove-bg --image produto.jpg
python generate.py --list-models
python generate.py --prompt "retrato fantasy" --analyze --json
Versao: 2.0.0
"""
from __future__ import annotations
import argparse
import base64
import io
import json
import re
import sys
import time
from datetime import datetime
from pathlib import Path
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
sys.path.insert(0, str(Path(__file__).parent))
from config import (
API_BASE,
ENDPOINTS,
MIME_MAP,
MODELS,
DEFAULT_MODEL,
OUTPUT_DIR,
OUTPUT_SETTINGS,
USER_AGENT,
get_api_key,
get_all_api_keys,
get_mime_type,
resolve_aspect_ratio,
safety_check_daily_limit,
increment_daily_counter,
validate_image_file,
)
from styles import apply_style, list_styles
# ── Exceptions ───────────────────────────────────────────────────────────────
class APIError(Exception):
"""Erro generico da API Stability AI."""
def __init__(self, message: str, status_code: int = 0):
super().__init__(message)
self.status_code = status_code
class RateLimitError(APIError):
"""Rate limit (429) atingido."""
pass
class ContentFilteredError(APIError):
"""Conteudo filtrado pela moderacao."""
pass
class AuthenticationError(APIError):
"""API key invalida ou ausente (401)."""
pass
class InsufficientCreditsError(APIError):
"""Creditos insuficientes (402)."""
pass
# ── API Call ─────────────────────────────────────────────────────────────────
def api_call(
endpoint: str,
api_key: str,
fields: dict,
files: dict | None = None,
accept: str = "image/*",
timeout: int = 180,
) -> tuple[bytes | dict, str, dict]:
"""
Faz chamada multipart/form-data para a Stability AI API.
Retorna (data, content_type, response_headers):
- Se accept="image/*": data = bytes da imagem
- Se accept="application/json": data = dict parseado
"""
url = f"{API_BASE}{endpoint}"
boundary = f"----StabilityBoundary{int(time.time() * 1000)}"
body = io.BytesIO()
# Campos de texto
for key, value in fields.items():
if value is None:
continue
body.write(f"--{boundary}\r\n".encode())
body.write(f'Content-Disposition: form-data; name="{key}"\r\n\r\n'.encode())
body.write(f"{value}\r\n".encode())
# Arquivos (imagens)
if files:
for key, filepath in files.items():
if filepath is None:
continue
filepath = Path(filepath)
validated = validate_image_file(filepath)
mime = get_mime_type(validated)
body.write(f"--{boundary}\r\n".encode())
body.write(
f'Content-Disposition: form-data; name="{key}"; '
f'filename="{validated.name}"\r\n'.encode()
)
body.write(f"Content-Type: {mime}\r\n\r\n".encode())
body.write(validated.read_bytes())
body.write(b"\r\n")
body.write(f"--{boundary}--\r\n".encode())
body_bytes = body.getvalue()
req = Request(
url,
data=body_bytes,
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": f"multipart/form-data; boundary={boundary}",
"Accept": accept,
"User-Agent": USER_AGENT,
},
method="POST",
)
try:
with urlopen(req, timeout=timeout) as resp:
content_type = resp.headers.get("Content-Type", "")
headers = dict(resp.headers)
data = resp.read()
if "application/json" in content_type:
return json.loads(data.decode("utf-8")), "application/json", headers
return data, content_type, headers
except HTTPError as e:
error_body = e.read().decode("utf-8", errors="replace")
# Mask API key in error output to prevent credential leakage
if api_key and api_key in error_body:
masked_key = f"{api_key[:6]}...masked" if len(api_key) >= 6 else "***masked***"
error_body = error_body.replace(api_key, masked_key)
try:
error_json = json.loads(error_body)
error_msg = json.dumps(error_json, indent=2, ensure_ascii=False)
except json.JSONDecodeError:
error_msg = error_body[:500]
if e.code == 401:
raise AuthenticationError(f"API key invalida ou ausente.\n{error_msg}", e.code)
if e.code == 402:
raise InsufficientCreditsError(f"Creditos insuficientes.\n{error_msg}", e.code)
if e.code == 403:
raise ContentFilteredError(f"Conteudo filtrado pela moderacao.\n{error_msg}", e.code)
if e.code == 429:
raise RateLimitError(f"Rate limit atingido.\n{error_msg}", e.code)
raise APIError(f"HTTP {e.code}: {error_msg}", e.code)
except URLError as e:
raise APIError(f"Erro de conexao: {e.reason}")
except TimeoutError:
raise APIError(f"Timeout ({timeout}s) na chamada para {url}")
# ── Geracao ──────────────────────────────────────────────────────────────────
def generate_image(
prompt: str,
mode: str = "generate",
model: str = DEFAULT_MODEL,
aspect_ratio: str = "1:1",
style: str | None = None,
negative_prompt: str | None = None,
image_path: str | None = None,
mask_path: str | None = None,
search_prompt: str | None = None,
strength: float | None = None,
seed: int | None = None,
raw: bool = False,
output_dir: Path | None = None,
api_key: str | None = None,
) -> list[dict]:
"""
Gera imagem(ns) e salva no disco.
Retorna lista de dicts com info de cada imagem:
[{"path": Path, "size_kb": float, "time_s": float, "seed": int|None}]
"""
output_dir = output_dir or OUTPUT_DIR
output_dir.mkdir(parents=True, exist_ok=True)
# Safety check
allowed, msg = safety_check_daily_limit(1)
if not allowed:
print(f"BLOQUEADO: {msg}", file=sys.stderr)
return []
# Aplicar estilo
final_prompt = prompt or ""
style_negative = None
if not raw and style:
final_prompt, style_negative = apply_style(final_prompt, style)
if not negative_prompt and style_negative:
negative_prompt = style_negative
# Obter API keys
keys = [api_key] if api_key else get_all_api_keys()
if not keys:
print(
"\nERRO: Nenhuma STABILITY_API_KEY encontrada!\n"
"Configure em .env ou variavel de ambiente.\n"
"Obtenha sua key gratuita em: https://platform.stability.ai\n",
file=sys.stderr,
)
return []
# Determinar endpoint
endpoint = _resolve_endpoint(mode, model)
# Montar campos e arquivos
fields, files = _build_request(
mode=mode, model=model, prompt=final_prompt,
aspect_ratio=aspect_ratio, negative_prompt=negative_prompt,
image_path=image_path, mask_path=mask_path,
search_prompt=search_prompt, strength=strength, seed=seed,
)
# Retry loop com fallback de keys
max_retries = 3
image_data = None
resp_headers: dict = {}
elapsed = 0.0
used_key_index = 0
for attempt in range(max_retries):
for i, key in enumerate(keys):
try:
start_time = time.time()
data, content_type, resp_headers = api_call(
endpoint=endpoint, api_key=key,
fields=fields, files=files, accept="image/*",
)
elapsed = time.time() - start_time
if isinstance(data, bytes) and len(data) > 100:
image_data = data
used_key_index = i
break
if isinstance(data, dict) and "image" in data:
image_data = base64.b64decode(data["image"])
used_key_index = i
break
print(f"Resposta inesperada (tipo: {content_type}, tamanho: {len(data) if isinstance(data, bytes) else 'dict'})",
file=sys.stderr)
except AuthenticationError as e:
print(f"Key {i+1} invalida: {e}", file=sys.stderr)
continue # Tentar proxima key
except InsufficientCreditsError as e:
print(f"ERRO: {e}", file=sys.stderr)
return [] # Nao adianta retry
except ContentFilteredError as e:
print(f"BLOQUEADO: Conteudo filtrado pela moderacao.\n{e}", file=sys.stderr)
return [] # Nao adianta retry
except RateLimitError:
wait = 15 * (attempt + 1)
print(f"Rate limit. Aguardando {wait}s...", file=sys.stderr)
time.sleep(wait)
break # Retry com todas as keys
except APIError as e:
is_last_key = i >= len(keys) - 1
if not is_last_key:
print(f"Key {i+1} falhou, tentando backup...", file=sys.stderr)
continue
if attempt < max_retries - 1:
wait = 5 * (attempt + 1)
print(f"Erro. Retry em {wait}s...", file=sys.stderr)
time.sleep(wait)
break
print(f"ERRO: {e}", file=sys.stderr)
return []
except Exception as e:
print(f"ERRO inesperado: {type(e).__name__}: {e}", file=sys.stderr)
if attempt >= max_retries - 1:
return []
time.sleep(5)
break
if image_data:
break
if not image_data:
print("ERRO: Nenhuma imagem gerada apos todas as tentativas.", file=sys.stderr)
return []
# Salvar imagem
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
style_tag = style or ("raw" if raw else "default")
filename = f"{mode}_{style_tag}_{timestamp}_0.png"
filepath = output_dir / filename
filepath.write_bytes(image_data)
increment_daily_counter(1)
size_kb = len(image_data) / 1024
resp_seed = resp_headers.get("seed") or resp_headers.get("Seed")
# Salvar metadados
if OUTPUT_SETTINGS["save_metadata"]:
metadata = {
"original_prompt": prompt,
"final_prompt": final_prompt,
"mode": mode,
"model": model,
"style": style,
"aspect_ratio": aspect_ratio,
"negative_prompt": negative_prompt,
"seed": resp_seed or seed,
"strength": strength,
"image_path": str(image_path) if image_path else None,
"mask_path": str(mask_path) if mask_path else None,
"search_prompt": search_prompt,
"raw": raw,
"generation_time_seconds": round(elapsed, 2),
"file_size_bytes": len(image_data),
"file_size_kb": round(size_kb, 1),
"generated_at": datetime.now().isoformat(),
"api_key_index": used_key_index,
"finish_reason": resp_headers.get("finish-reason", "SUCCESS"),
"skill_version": "2.0.0",
}
meta_path = output_dir / f"{filename}.meta.json"
meta_path.write_text(
json.dumps(metadata, indent=2, ensure_ascii=False, default=str),
encoding="utf-8",
)
print(f"Imagem salva: {filepath}")
print(f"Tamanho: {size_kb:.1f} KB")
print(f"Tempo: {elapsed:.1f}s")
return [{"path": filepath, "size_kb": round(size_kb, 1), "time_s": round(elapsed, 1), "seed": resp_seed}]
# ── Helpers ──────────────────────────────────────────────────────────────────
def _resolve_endpoint(mode: str, model: str) -> str:
"""Determina o endpoint da API com base no modo e modelo."""
mode_map = {
"generate": MODELS.get(model, {}).get("endpoint", "generate_sd3"),
"ultra": "generate_ultra",
"core": "generate_core",
"img2img": "generate_sd3",
"upscale": "upscale_conservative",
"upscale-creative": "upscale_creative",
"remove-bg": "remove_bg",
"inpaint": "inpaint",
"search-replace": "search_replace",
"erase": "erase",
}
endpoint_key = mode_map.get(mode, "generate_sd3")
return ENDPOINTS.get(endpoint_key, ENDPOINTS["generate_sd3"])
def _build_request(
mode: str,
model: str,
prompt: str,
aspect_ratio: str,
negative_prompt: str | None,
image_path: str | None,
mask_path: str | None,
search_prompt: str | None,
strength: float | None,
seed: int | None,
) -> tuple[dict, dict | None]:
"""Monta campos e arquivos para a request multipart."""
fields: dict = {}
files: dict | None = None
# Campos comuns de texto
common_text_fields = {}
if negative_prompt:
common_text_fields["negative_prompt"] = negative_prompt
if seed is not None:
common_text_fields["seed"] = str(seed)
common_text_fields["output_format"] = "png"
if mode in ("generate", "img2img"):
fields["prompt"] = prompt
fields.update(common_text_fields)
if mode == "generate":
fields["aspect_ratio"] = aspect_ratio
model_config = MODELS.get(model, MODELS[DEFAULT_MODEL])
if model_config["endpoint"] == "generate_sd3":
fields["model"] = model_config["id"]
if mode == "img2img" and image_path:
fields["mode"] = "image-to-image"
if strength is not None:
fields["strength"] = str(min(max(strength, 0.0), 1.0))
files = {"image": image_path}
elif mode in ("ultra", "core"):
fields["prompt"] = prompt
fields["aspect_ratio"] = aspect_ratio
fields.update(common_text_fields)
elif mode in ("upscale", "upscale-creative"):
fields.update(common_text_fields)
if prompt:
fields["prompt"] = prompt
files = {"image": image_path}
elif mode == "remove-bg":
fields["output_format"] = "png"
files = {"image": image_path}
elif mode == "inpaint":
fields["prompt"] = prompt
fields.update(common_text_fields)
files = {"image": image_path}
if mask_path:
files["mask"] = mask_path
elif mode == "search-replace":
fields["prompt"] = prompt
fields.update(common_text_fields)
if search_prompt:
fields["search_prompt"] = search_prompt
files = {"image": image_path}
elif mode == "erase":
fields["output_format"] = "png"
files = {"image": image_path}
if mask_path:
files["mask"] = mask_path
return fields, files
def analyze_prompt(prompt: str) -> dict:
"""Analisa prompt e sugere configuracoes ideais."""
prompt_lower = prompt.lower()
# Detectar estilo
style = None
style_hints = {
"photorealistic": ["foto", "photo", "realistic", "camera", "portrait", "dslr"],
"anime": ["anime", "manga", "ghibli", "kawaii", "chibi", "otaku"],
"digital-art": ["digital", "artstation", "deviantart", "digital art"],
"oil-painting": ["oil", "oleo", "canvas", "pintura classica"],
"watercolor": ["watercolor", "aquarela", "wash", "aguada"],
"pixel-art": ["pixel", "8-bit", "16-bit", "retro game", "sprite"],
"3d-render": ["3d", "render", "blender", "unreal", "octane", "cinema4d"],
"concept-art": ["concept", "concept art", "game art", "matte painting"],
"comic": ["comic", "hq", "quadrinho", "manga style", "graphic novel"],
"fantasy": ["fantasy", "magic", "dragon", "elf", "medieval", "enchanted"],
"sci-fi": ["sci-fi", "cyberpunk", "futuristic", "space", "neon", "cyber"],
"sketch": ["sketch", "pencil", "drawing", "charcoal", "lapis", "rascunho"],
"noir": ["noir", "black and white", "detective", "moody", "shadows"],
"pop-art": ["pop art", "warhol", "bold colors", "vibrante"],
"minimalist": ["minimalist", "clean", "simple", "flat design"],
}
for style_name, keywords in style_hints.items():
if any(kw in prompt_lower for kw in keywords):
style = style_name
break
# Detectar aspect ratio
ratio = "1:1"
ratio_hints = {
"16:9": ["landscape", "paisagem", "wide", "panorama", "cinema", "wallpaper", "widescreen"],
"9:16": ["portrait", "retrato", "vertical", "stories", "mobile", "phone", "tiktok", "reels"],
"2:3": ["poster", "book", "cover", "pinterest", "cartaz"],
"3:2": ["photo", "foto", "horizontal", "banner"],
"4:5": ["instagram", "ig", "feed"],
}
for r, keywords in ratio_hints.items():
if any(kw in prompt_lower for kw in keywords):
ratio = r
break
# Detectar modelo
suggested_model = "sd3.5-large"
if any(kw in prompt_lower for kw in ["ultra", "premium", "best quality", "8k", "4k", "maximum"]):
suggested_model = "ultra"
elif any(kw in prompt_lower for kw in ["quick", "fast", "rapido", "draft", "rascunho"]):
suggested_model = "sd3.5-large-turbo"
elif any(kw in prompt_lower for kw in ["core", "simple", "simples", "basico"]):
suggested_model = "core"
# Detectar modo
suggested_mode = "generate"
if any(kw in prompt_lower for kw in ["upscale", "increase resolution", "melhorar resolucao", "aumentar"]):
suggested_mode = "upscale"
elif any(kw in prompt_lower for kw in ["remove background", "remover fundo", "sem fundo", "transparente"]):
suggested_mode = "remove-bg"
elif any(kw in prompt_lower for kw in ["inpaint", "editar parte", "modificar area"]):
suggested_mode = "inpaint"
elif any(kw in prompt_lower for kw in ["replace", "substituir", "trocar"]):
suggested_mode = "search-replace"
elif any(kw in prompt_lower for kw in ["erase", "apagar", "remover objeto"]):
suggested_mode = "erase"
return {
"suggested_style": style,
"suggested_aspect_ratio": ratio,
"suggested_model": suggested_model,
"suggested_mode": suggested_mode,
"prompt": prompt,
}
# ── CLI ──────────────────────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(
description="Gerar imagens via Stability AI (Stable Diffusion 3.5, Ultra, Core)",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=(
"Exemplos:\n"
' python generate.py --prompt "mountain sunset" --mode generate\n'
' python generate.py --prompt "watercolor cat" --style watercolor\n'
' python generate.py --prompt "epic portrait" --mode ultra --aspect-ratio wide\n'
' python generate.py --mode upscale --image foto.jpg\n'
' python generate.py --mode remove-bg --image produto.jpg\n'
" python generate.py --list-models\n"
" python generate.py --list-styles\n"
),
)
# Principal
parser.add_argument("--prompt", type=str, help="Prompt de texto para geracao")
parser.add_argument(
"--mode", type=str, default="generate",
choices=["generate", "ultra", "core", "img2img", "upscale", "upscale-creative",
"remove-bg", "inpaint", "search-replace", "erase"],
help="Modo de geracao (default: generate)",
)
# Modelo e estilo
parser.add_argument("--model", type=str, default=DEFAULT_MODEL, help=f"Modelo (default: {DEFAULT_MODEL})")
parser.add_argument("--style", type=str, default=None, help="Estilo pre-configurado")
parser.add_argument("--aspect-ratio", type=str, default="1:1", help="Aspect ratio (ex: 16:9, square, ig)")
parser.add_argument("--negative-prompt", type=str, default=None, help="O que evitar na imagem")
parser.add_argument("--seed", type=int, default=None, help="Seed para reprodutibilidade")
parser.add_argument("--strength", type=float, default=None, help="Forca para img2img (0.0-1.0)")
parser.add_argument("--raw", action="store_true", help="Nao aplicar estilo, usar prompt como esta")
# Imagens de entrada
parser.add_argument("--image", type=str, default=None, help="Imagem de entrada")
parser.add_argument("--mask", type=str, default=None, help="Mascara para inpainting/erase")
parser.add_argument("--search", type=str, default=None, help="Texto para search-and-replace")
# Output
parser.add_argument("--output", type=Path, default=None, help="Diretorio de saida")
# Utilidades
parser.add_argument("--analyze", action="store_true", help="Analisar prompt e sugerir config")
parser.add_argument("--list-models", action="store_true", help="Listar modelos disponiveis")
parser.add_argument("--list-styles", action="store_true", help="Listar estilos disponiveis")
parser.add_argument("--json", action="store_true", help="Saida em JSON")
args = parser.parse_args()
# --- Utilidades ---
if args.list_models:
if args.json:
print(json.dumps(MODELS, indent=2, ensure_ascii=False))
else:
print("\n Modelos Disponiveis:\n")
for key, m in MODELS.items():
print(f" {key:25s} {m['name']}")
print(f" {'':25s} {m['description']}")
print(f" {'':25s} Custo: {m['cost']}\n")
return
if args.list_styles:
styles = list_styles()
if args.json:
print(json.dumps(styles, indent=2, ensure_ascii=False))
else:
print("\n Estilos Disponiveis:\n")
for key, s in styles.items():
print(f" {key:20s} {s['name']}")
return
if args.analyze:
if not args.prompt:
print("ERRO: --analyze requer --prompt", file=sys.stderr)
sys.exit(1)
result = analyze_prompt(args.prompt)
if args.json:
print(json.dumps(result, indent=2, ensure_ascii=False))
else:
print("\n Analise do Prompt:\n")
for k, v in result.items():
print(f" {k:30s} {v}")
return
# --- Validacao ---
needs_prompt = args.mode in ("generate", "ultra", "core", "img2img", "inpaint", "search-replace")
needs_image = args.mode in ("img2img", "upscale", "upscale-creative", "remove-bg", "inpaint", "search-replace", "erase")
if needs_prompt and not args.prompt:
print(f"ERRO: modo '{args.mode}' requer --prompt", file=sys.stderr)
sys.exit(1)
if needs_image and not args.image:
print(f"ERRO: modo '{args.mode}' requer --image", file=sys.stderr)
sys.exit(1)
# --- Execucao ---
aspect = resolve_aspect_ratio(args.aspect_ratio)
print("=" * 60)
print(" STABILITY AI - Gerando Imagem")
print("=" * 60)
print(f" Modo: {args.mode}")
print(f" Modelo: {args.model}")
if args.style:
print(f" Estilo: {args.style}")
print(f" Aspect Ratio: {aspect}")
if args.image:
print(f" Imagem input: {args.image}")
print("=" * 60)
print()
results = generate_image(
prompt=args.prompt or "",
mode=args.mode, model=args.model, aspect_ratio=aspect,
style=args.style, negative_prompt=args.negative_prompt,
image_path=args.image, mask_path=args.mask,
search_prompt=args.search, strength=args.strength,
seed=args.seed, raw=args.raw, output_dir=args.output,
)
if args.json:
output = {
"generated": [str(r["path"]) for r in results],
"count": len(results),
"output_dir": str(results[0]["path"].parent) if results else None,
"details": [{
"path": str(r["path"]),
"size_kb": r["size_kb"],
"time_s": r["time_s"],
"seed": r.get("seed"),
} for r in results],
}
print(json.dumps(output, indent=2, ensure_ascii=False))
if not results:
sys.exit(1)
if __name__ == "__main__":
main()