feat(ml): núcleo subsistema comfyui-skill + ask_llm_vision
Grupo nuevo comfyui-skill: recetas versionadas de generación ComfyUI que
compilan a un workflow cambiando solo el subject.
- comfyui_build_skill_workflow (pura): receta -> workflow API format,
despacha base (txt2img/flux/sdxl_refiner), sustituye {subject}+triggers,
encadena loras e inject blocks (facedetailer, hires_fix). SkillWorkflowError tipada.
- comfyui_inject_hires_fix (pura): inyecta 2ª pasada UltimateSDUpscale sobre dict.
- comfyui_save/load/list_skill (impuras): CRUD de la librería en disco con
versionado por snapshots, round-trip idéntico, filtro NSFW.
- ask_llm_vision (core, claude-direct): pregunta multimodal imagen+texto via
API directa Anthropic, para puntuar generaciones.
- Página madre docs/capabilities/comfyui-skill.md con schema canónico de recipe.json.
Tests offline: 11 verdes (6 builder + 5 inject_hires_fix). Sin GPU.
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
"""comfyui_load_skill — lee una receta de *skill* ComfyUI de la libreria de disco.
|
||||
|
||||
Carga `recipe.json` (version actual) o un snapshot concreto `versions/vN.json` de una skill
|
||||
guardada por `comfyui_save_skill`. Hermana inversa de save: el round-trip
|
||||
save(recipe) -> load(slug) devuelve un dict identico al guardado.
|
||||
|
||||
`library_dir` por defecto `~/ComfyUI/skills_library`. Un slug inexistente devuelve
|
||||
``{ok: False}`` sin lanzar excepcion.
|
||||
|
||||
Impura: lee archivos de disco.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
DEFAULT_LIBRARY = "~/ComfyUI/skills_library"
|
||||
|
||||
|
||||
def _lib_dir(library_dir):
|
||||
return os.path.expanduser(library_dir or DEFAULT_LIBRARY)
|
||||
|
||||
|
||||
def _version_filename(version):
|
||||
"""Normaliza la version a un nombre de archivo `vN.json`.
|
||||
|
||||
Acepta int (1), str de digitos ("1") o ya prefijada ("v1"). Devuelve None si no
|
||||
se puede interpretar.
|
||||
"""
|
||||
if isinstance(version, int):
|
||||
return f"v{version}.json"
|
||||
s = str(version).strip()
|
||||
if s.startswith("v"):
|
||||
s = s[1:]
|
||||
if s.isdigit():
|
||||
return f"v{s}.json"
|
||||
return None
|
||||
|
||||
|
||||
def comfyui_load_skill(slug: str, *, version=None, library_dir: str = None) -> dict:
|
||||
"""Lee la receta de una skill (version actual o un snapshot concreto).
|
||||
|
||||
Args:
|
||||
slug: slug de la skill (nombre de su carpeta en la libreria).
|
||||
version: si None, lee `recipe.json` (version actual). Si se pasa (int, "1" o
|
||||
"v1"), lee el snapshot `versions/vN.json`. keyword-only.
|
||||
library_dir: raiz de la libreria. Default `~/ComfyUI/skills_library`. keyword-only.
|
||||
|
||||
Returns:
|
||||
dict ``{ok, recipe, slug, path, version, error}``. En exito ``ok=True`` y `recipe`
|
||||
es el dict guardado. Si el slug, la version o el archivo no existen, ``ok=False``,
|
||||
``recipe=None`` y ``error`` describe la causa; nunca lanza.
|
||||
"""
|
||||
if not slug or not isinstance(slug, str):
|
||||
return {"ok": False, "recipe": None, "slug": slug, "path": "", "version": version,
|
||||
"error": "slug requerido (string no vacio)"}
|
||||
|
||||
lib = _lib_dir(library_dir)
|
||||
skill_dir = os.path.join(lib, slug)
|
||||
if not os.path.isdir(skill_dir):
|
||||
return {"ok": False, "recipe": None, "slug": slug, "path": skill_dir, "version": version,
|
||||
"error": f"skill no encontrada: {slug!r}"}
|
||||
|
||||
if version is None:
|
||||
target = os.path.join(skill_dir, "recipe.json")
|
||||
else:
|
||||
fname = _version_filename(version)
|
||||
if fname is None:
|
||||
return {"ok": False, "recipe": None, "slug": slug, "path": skill_dir,
|
||||
"version": version, "error": f"version invalida: {version!r}"}
|
||||
target = os.path.join(skill_dir, "versions", fname)
|
||||
|
||||
if not os.path.isfile(target):
|
||||
return {"ok": False, "recipe": None, "slug": slug, "path": target, "version": version,
|
||||
"error": f"archivo de receta no encontrado: {target}"}
|
||||
|
||||
try:
|
||||
with open(target, encoding="utf-8") as fh:
|
||||
recipe = json.load(fh)
|
||||
except (OSError, json.JSONDecodeError) as exc:
|
||||
return {"ok": False, "recipe": None, "slug": slug, "path": target, "version": version,
|
||||
"error": f"no se pudo leer la receta: {exc}"}
|
||||
|
||||
return {"ok": True, "recipe": recipe, "slug": slug, "path": target, "version": version,
|
||||
"error": ""}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
res = comfyui_load_skill("demo_skill", library_dir="/tmp/skills_demo")
|
||||
print(json.dumps(res, indent=2, ensure_ascii=False))
|
||||
Reference in New Issue
Block a user