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:
agent
2026-06-27 13:50:25 +02:00
parent a5748cb147
commit d3d846f748
9 changed files with 1197 additions and 0 deletions
@@ -0,0 +1,134 @@
"""Tests offline de comfyui_append_styles — no toca la instalación real ni la red.
Usa un styles.json temporal en /tmp para validar merge, dedup, backup, validación y dry-run.
"""
import json
import os
import sys
import tempfile
sys.path.insert(0, os.path.dirname(__file__))
from comfyui_append_styles import comfyui_append_styles, DEFAULT_NEGATIVE
def _write_styles(tmpdir: str, data: dict) -> str:
path = os.path.join(tmpdir, "styles.json")
with open(path, "w", encoding="utf-8") as fh:
json.dump(data, fh, ensure_ascii=False, indent=4)
return path
def test_merge_preserva_existentes_y_anade_nuevos():
with tempfile.TemporaryDirectory() as d:
path = _write_styles(d, {
"A": {"prompt": "a-style", "negative_prompt": "neg-a"},
"B": {"prompt": "b-style", "negative_prompt": "neg-b"},
})
res = comfyui_append_styles(
{"C": {"prompt": "c-style", "negative_prompt": "neg-c"}},
styles_path=path,
)
assert res["total_before"] == 2
assert res["total_after"] == 3
assert res["added"] == ["C"]
loaded = json.load(open(path, encoding="utf-8"))
# Los existentes intactos.
assert loaded["A"] == {"prompt": "a-style", "negative_prompt": "neg-a"}
assert loaded["B"] == {"prompt": "b-style", "negative_prompt": "neg-b"}
assert loaded["C"] == {"prompt": "c-style", "negative_prompt": "neg-c"}
def test_dedup_no_pisa_por_defecto():
with tempfile.TemporaryDirectory() as d:
path = _write_styles(d, {"A": {"prompt": "orig", "negative_prompt": "n"}})
res = comfyui_append_styles(
{"A": {"prompt": "NUEVO", "negative_prompt": "n2"}},
styles_path=path,
)
assert res["skipped_existing"] == ["A"]
assert res["added"] == []
assert res["total_after"] == 1
loaded = json.load(open(path, encoding="utf-8"))
assert loaded["A"]["prompt"] == "orig" # preservado
def test_overwrite_si_se_pide():
with tempfile.TemporaryDirectory() as d:
path = _write_styles(d, {"A": {"prompt": "orig", "negative_prompt": "n"}})
res = comfyui_append_styles(
{"A": {"prompt": "NUEVO", "negative_prompt": "n2"}},
styles_path=path,
overwrite=True,
)
assert res["overwritten"] == ["A"]
loaded = json.load(open(path, encoding="utf-8"))
assert loaded["A"]["prompt"] == "NUEVO"
def test_negative_por_defecto_cuando_falta():
with tempfile.TemporaryDirectory() as d:
path = _write_styles(d, {})
res = comfyui_append_styles(
{"X": {"prompt": "solo-prompt"}}, # sin negative_prompt
styles_path=path,
)
assert res["added"] == ["X"]
loaded = json.load(open(path, encoding="utf-8"))
assert loaded["X"]["negative_prompt"] == DEFAULT_NEGATIVE
def test_entradas_invalidas_se_descartan():
with tempfile.TemporaryDirectory() as d:
path = _write_styles(d, {})
res = comfyui_append_styles(
{
"ok": {"prompt": "valido"},
"vacio": {"prompt": " "}, # prompt vacío
"no_dict": "string", # no es dict
"sin_prompt": {"negative_prompt": "n"},
},
styles_path=path,
)
assert res["added"] == ["ok"]
assert set(res["invalid"]) == {"vacio", "no_dict", "sin_prompt"}
assert res["total_after"] == 1
def test_backup_creado():
with tempfile.TemporaryDirectory() as d:
path = _write_styles(d, {"A": {"prompt": "a", "negative_prompt": "n"}})
res = comfyui_append_styles(
{"B": {"prompt": "b"}},
styles_path=path,
)
assert res["backup_path"]
assert os.path.isfile(res["backup_path"])
# El backup contiene el estado ANTERIOR (sólo A).
bk = json.load(open(res["backup_path"], encoding="utf-8"))
assert list(bk) == ["A"]
def test_dry_run_no_escribe():
with tempfile.TemporaryDirectory() as d:
path = _write_styles(d, {"A": {"prompt": "a", "negative_prompt": "n"}})
before = open(path, encoding="utf-8").read()
res = comfyui_append_styles(
{"B": {"prompt": "b"}},
styles_path=path,
dry_run=True,
)
assert res["dry_run"] is True
assert res["added"] == ["B"]
assert res["total_after"] == 2 # calculado
assert res["backup_path"] == ""
after = open(path, encoding="utf-8").read()
assert before == after # archivo intacto
if __name__ == "__main__":
for name, fn in sorted(globals().items()):
if name.startswith("test_") and callable(fn):
fn()
print("PASS", name)
print("OK")