d3d846f748
Tres funciones para gestionar y ampliar el repositorio de estilos del selector
WAS de ComfyUI (Prompt Styles Selector / Prompt Multiple Styles Selector):
- comfyui_curated_styles_catalog (pure): catálogo curado de 190 estilos en 13
categorías (photography, render3d, painting, anime, pixel, illustration,
comic, lighting, camera, material, scifi, fantasy, mood), formato WAS exacto.
- comfyui_append_styles (impure): merge+dedup no destructivo sobre el styles.json
real, con backup atómico, validación de entradas y preservación de existentes.
- comfyui_generate_styles_llm (impure): genera estilos de una categoría vía
ask_llm (grupo claude-direct); robusta (devuelve {} ante 429/JSON corrupto).
Aplicado en vivo: styles.json 269 -> 503 estilos (+190 curados +44 LLM),
backup hecho, selector verifica 503 en /object_info. Tests offline verdes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
157 lines
6.5 KiB
Python
157 lines
6.5 KiB
Python
"""comfyui_generate_styles_llm — genera estilos WAS para ComfyUI con el LLM (grupo claude-direct).
|
|
|
|
Pide al modelo (vía `ask_llm`, API directa de Anthropic, arranque 0) que produzca N estilos de
|
|
una CATEGORÍA temática en el formato exacto del selector WAS:
|
|
|
|
{ "NombreEstilo": {"prompt": "modificadores de estilo", "negative_prompt": "..."}, ... }
|
|
|
|
Cada `prompt` son MODIFICADORES de estilo (cámara, lente, iluminación, render, medio artístico,
|
|
paleta, mood) — NO una descripción de sujeto y SIN el placeholder `{prompt}` (el selector
|
|
múltiple concatena los prompts elegidos).
|
|
|
|
Robusta por diseño: extrae el primer bloque JSON de la respuesta, lo valida entrada por entrada
|
|
(descarta las que no tengan `prompt`, rellena `negative_prompt` por defecto si falta) y, ante
|
|
CUALQUIER fallo (rate-limit 429, JSON corrupto, respuesta vacía), devuelve `{}` en vez de lanzar.
|
|
Así el caller puede iterar varias categorías y seguir aunque una falle (error-path del DoD).
|
|
|
|
Impura: llama a la API del LLM (red). No escribe disco (eso es trabajo de
|
|
`comfyui_append_styles`). Pensada para componer: generar -> validar -> append.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
# Importa ask_llm del registry (grupo claude-direct). Path al paquete de funciones Python.
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
|
|
try:
|
|
from core.ask_llm import ask_llm # type: ignore
|
|
except Exception: # pragma: no cover - sólo si el registry no está en el path
|
|
ask_llm = None # type: ignore
|
|
|
|
DEFAULT_NEGATIVE = (
|
|
"ugly, deformed, noisy, blurry, low quality, distorted, disfigured, "
|
|
"bad anatomy, watermark, signature, text, NSFW"
|
|
)
|
|
|
|
_SYSTEM = (
|
|
"You are an expert prompt engineer for Stable Diffusion. You output ONLY valid JSON, "
|
|
"no prose, no markdown fences. Styles must be SFW."
|
|
)
|
|
|
|
_PROMPT_TMPL = """Generate exactly {n} distinct image STYLE presets for the category: "{category}".
|
|
|
|
Output a single JSON object mapping a short unique style name to an object with keys
|
|
"prompt" and "negative_prompt". Rules:
|
|
- "prompt" = a comma-separated list of powerful STYLE modifiers ONLY (camera, lens, lighting,
|
|
render engine, art medium, palette, texture, mood). NO subject description. Do NOT include
|
|
the literal token {{prompt}}.
|
|
- "negative_prompt" = comma-separated terms to avoid for this style. Keep it SFW.
|
|
- Style names must be short, kebab-case, prefixed with "{prefix}", and must NOT repeat these
|
|
existing names: {avoid}.
|
|
- Output ONLY the JSON object, nothing else.
|
|
|
|
Example of ONE entry:
|
|
"{prefix}example": {{"prompt": "cinematic film still, anamorphic lens, teal and orange grade, dramatic key light", "negative_prompt": "lowres, blurry, deformed, watermark, text"}}
|
|
"""
|
|
|
|
|
|
def _extract_json_object(text: str) -> dict | None:
|
|
"""Extrae el primer objeto JSON {...} de un texto, tolerando fences de markdown."""
|
|
if not text:
|
|
return None
|
|
# Quitar fences ```json ... ``` si los hay.
|
|
fenced = re.search(r"```(?:json)?\s*(\{.*\})\s*```", text, re.DOTALL)
|
|
candidate = fenced.group(1) if fenced else None
|
|
if candidate is None:
|
|
# Buscar desde la primera { hasta la última } (greedy) — robusto a prosa alrededor.
|
|
start = text.find("{")
|
|
end = text.rfind("}")
|
|
if start == -1 or end == -1 or end <= start:
|
|
return None
|
|
candidate = text[start : end + 1]
|
|
try:
|
|
obj = json.loads(candidate)
|
|
except (json.JSONDecodeError, ValueError):
|
|
return None
|
|
return obj if isinstance(obj, dict) else None
|
|
|
|
|
|
def _validate(obj: dict, avoid: set[str]) -> dict:
|
|
"""Filtra el dict del LLM a entradas válidas WAS, descartando duplicados y vacíos."""
|
|
clean: dict[str, dict[str, str]] = {}
|
|
for name, v in obj.items():
|
|
if not isinstance(name, str) or not name.strip():
|
|
continue
|
|
if name in avoid:
|
|
continue
|
|
if not isinstance(v, dict):
|
|
continue
|
|
prompt = v.get("prompt")
|
|
if not isinstance(prompt, str) or not prompt.strip():
|
|
continue
|
|
if "{prompt}" in prompt:
|
|
prompt = prompt.replace("{prompt}", "").strip().strip(",").strip()
|
|
if not prompt:
|
|
continue
|
|
neg = v.get("negative_prompt")
|
|
if not isinstance(neg, str) or not neg.strip():
|
|
neg = DEFAULT_NEGATIVE
|
|
clean[name.strip()] = {"prompt": prompt.strip(), "negative_prompt": neg.strip()}
|
|
return clean
|
|
|
|
|
|
def comfyui_generate_styles_llm(
|
|
category: str,
|
|
n: int = 8,
|
|
prefix: str = "",
|
|
avoid: list[str] | None = None,
|
|
model: str = "claude-haiku-4-5-20251001",
|
|
) -> dict:
|
|
"""Genera estilos WAS de una categoría con el LLM. Devuelve {} ante cualquier fallo.
|
|
|
|
Args:
|
|
category: tema de los estilos (ej. "vaporwave aesthetics", "baroque painting",
|
|
"macro insect photography"). Texto libre, dirige el contenido.
|
|
n: número de estilos a pedir (el modelo puede devolver menos; se validan todos).
|
|
prefix: prefijo para los nombres generados (ej. "vapor-"), para agrupar y reducir
|
|
colisiones con estilos existentes. Si vacío, los nombres van sin prefijo.
|
|
avoid: lista de nombres ya existentes que el modelo NO debe repetir (dedup previo).
|
|
model: id del modelo Anthropic. Default haiku (más cuota, rápido). Otros:
|
|
claude-opus-4-8, claude-sonnet-4-6.
|
|
|
|
Returns:
|
|
dict {nombre: {"prompt", "negative_prompt"}} ya validado (prompt no vacío, sin
|
|
`{prompt}`, negative rellenado). Vacío `{}` si el LLM falla, la respuesta no trae JSON
|
|
válido, o no hay entradas utilizables. NUNCA lanza: el error-path es devolver {}.
|
|
"""
|
|
if ask_llm is None:
|
|
return {}
|
|
avoid_set = set(avoid or [])
|
|
avoid_str = ", ".join(sorted(avoid_set)[:40]) if avoid_set else "(none)"
|
|
prompt = _PROMPT_TMPL.format(
|
|
n=int(n), category=category, prefix=prefix, avoid=avoid_str
|
|
)
|
|
try:
|
|
raw = ask_llm(prompt, model=model, system=_SYSTEM, max_tokens=4096, echo=False)
|
|
except Exception:
|
|
return {}
|
|
obj = _extract_json_object(raw or "")
|
|
if obj is None:
|
|
return {}
|
|
return _validate(obj, avoid_set)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import json as _json
|
|
|
|
cat = sys.argv[1] if len(sys.argv) > 1 else "synthwave retro aesthetics"
|
|
n = int(sys.argv[2]) if len(sys.argv) > 2 else 6
|
|
pref = sys.argv[3] if len(sys.argv) > 3 else "synth-"
|
|
out = comfyui_generate_styles_llm(cat, n=n, prefix=pref)
|
|
print(_json.dumps(out, ensure_ascii=False, indent=2))
|
|
print("GENERADOS:", len(out), file=sys.stderr)
|