feat(ml): grupo comfyui-styles — catálogo curado + merge/dedup + generador LLM de estilos WAS
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>
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user