feat(ml): cierre del bucle de mejora comfyui-skill (genera→juzga→bump)
Tres funciones nuevas que cierran el lazo skill→generación→juicio→promoción del grupo comfyui-skill (issue 0087): - comfyui_bump_skill_version (impura): promueve una versión nueva SOLO si el score del panel-juez sube (gate objetivo). Snapshot versions/vN.json pre-mutación, deep-merge de recipe_patch, semver↑, línea en growth_log.jsonl. force=True salta el gate. No usa datetime.now(). - comfyui_update_skill_score (impura): media incremental de score_mean/score_n reescribiendo recipe.json in-place (sin snapshot ni growth_log). - comfyui_generate_with_skill_oneshot (pipeline): one-shot load→build→submit→ wait→fetch→judge→score_mean. recipe_patch prueba variantes sin guardar score. Compone 7 funciones del registry. Tests offline: 11 passed (gate, semver, deep-merge, media incremental, errores). Página madre docs/capabilities/comfyui-skill.md: +3 funciones, sección "Bucle de mejora" con diagrama, fronteras de scoring actualizadas. Demo real verificada: skill seed portrait_cinematic_sd15 (SD1.5) generó imagen SFW real, el panel la juzgó, una variante puntuó más alto (4.787 > 4.7276) y el gate promovió v1.0.0→v1.1.0 con el judge_run_id como evidencia. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -63,6 +63,9 @@ de texto). `blocks[].type` ∈ {`facedetailer`, `hires_fix`}.
|
|||||||
| [comfyui_load_skill_py_ml](../../python/functions/ml/comfyui_load_skill.md) | `comfyui_load_skill(slug, *, version=None, library_dir=None) -> dict` | Lee `recipe.json` (actual) o un snapshot `versions/vN.json`. Slug/versión inexistente → `{ok:False}` sin lanzar. | impura |
|
| [comfyui_load_skill_py_ml](../../python/functions/ml/comfyui_load_skill.md) | `comfyui_load_skill(slug, *, version=None, library_dir=None) -> dict` | Lee `recipe.json` (actual) o un snapshot `versions/vN.json`. Slug/versión inexistente → `{ok:False}` sin lanzar. | impura |
|
||||||
| [comfyui_list_skills_py_ml](../../python/functions/ml/comfyui_list_skills.md) | `comfyui_list_skills(*, library_dir=None, include_nsfw=False) -> dict` | Lista las skills con slug/title/base_workflow/version/score/nsfw/n_versions. Oculta NSFW por defecto. | impura |
|
| [comfyui_list_skills_py_ml](../../python/functions/ml/comfyui_list_skills.md) | `comfyui_list_skills(*, library_dir=None, include_nsfw=False) -> dict` | Lista las skills con slug/title/base_workflow/version/score/nsfw/n_versions. Oculta NSFW por defecto. | impura |
|
||||||
| [ask_llm_vision_py_core](../../python/functions/core/ask_llm_vision.md) | `ask_llm_vision(prompt, image_path='', *, image_b64='', media_type='', model='claude-opus-4-8', ...) -> dict` | Pregunta multimodal (imagen + texto) al modelo via API directa de Anthropic (grupo `claude-direct`). Útil para **puntuar** el PNG de una skill y alimentar `score_mean`. | impura |
|
| [ask_llm_vision_py_core](../../python/functions/core/ask_llm_vision.md) | `ask_llm_vision(prompt, image_path='', *, image_b64='', media_type='', model='claude-opus-4-8', ...) -> dict` | Pregunta multimodal (imagen + texto) al modelo via API directa de Anthropic (grupo `claude-direct`). Útil para **puntuar** el PNG de una skill y alimentar `score_mean`. | impura |
|
||||||
|
| [comfyui_generate_with_skill_oneshot_py_pipelines](../../python/functions/pipelines/comfyui_generate_with_skill_oneshot.md) | `generate_with_skill_oneshot(slug, subject, *, server='127.0.0.1:8188', dest=None, seed=0, judge=True, recipe_patch=None, ...) -> dict` | One-shot del bucle: carga la skill, la compila para el `subject`, encola, espera, descarga el PNG y (si `judge`) lo puntúa con el panel `comfyui-judge`, acumulando el score en la media. `recipe_patch` prueba una variante en memoria sin guardar. | pipeline (impura) |
|
||||||
|
| [comfyui_update_skill_score_py_ml](../../python/functions/ml/comfyui_update_skill_score.md) | `comfyui_update_skill_score(slug, new_score, *, library_dir=None) -> dict` | Acumula el score de un juicio en `score_mean`/`score_n` por media incremental, reescribiendo `recipe.json` en sitio (sin snapshot ni growth_log). | impura |
|
||||||
|
| [comfyui_bump_skill_version_py_ml](../../python/functions/ml/comfyui_bump_skill_version.md) | `comfyui_bump_skill_version(slug, change, *, score_before, score_after, judge_run_id=None, recipe_patch=None, force=False, ...) -> dict` | Promueve una versión nueva **solo si el score sube** (gate objetivo): snapshot `versions/vN.json` + aplica `recipe_patch` + sube el semver + línea en `growth_log`. Gate bloquea si no mejora. | impura |
|
||||||
|
|
||||||
`build_skill_workflow` compone los builders del grupo [`comfyui`](comfyui.md):
|
`build_skill_workflow` compone los builders del grupo [`comfyui`](comfyui.md):
|
||||||
`comfyui_build_txt2img_workflow`, `comfyui_build_flux_workflow`,
|
`comfyui_build_txt2img_workflow`, `comfyui_build_flux_workflow`,
|
||||||
@@ -120,6 +123,47 @@ print(verdict["text"])
|
|||||||
El paso "guardar la receta" se hace una sola vez; a partir de ahí cada generación es
|
El paso "guardar la receta" se hace una sola vez; a partir de ahí cada generación es
|
||||||
`load → build → submit`, cambiando solo el `subject` y la `seed`.
|
`load → build → submit`, cambiando solo el `subject` y la `seed`.
|
||||||
|
|
||||||
|
## Bucle de mejora (skill → genera → juzga → bump)
|
||||||
|
|
||||||
|
La doctrina del issue 0087 cerrada en un lazo: una skill **no crece inflando la receta a ciegas,
|
||||||
|
crece registrando mejoras medibles**. El juez (no el humano) decide qué se promueve.
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ generate_with_skill_oneshot(slug, subject, judge=True) │
|
||||||
|
│ load → build → submit → wait → fetch → judge → score_mean │ ← canónica
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
│ score_before = score de la receta vigente
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ generate_with_skill_oneshot(..., recipe_patch={params:{...}}) │ ← variante (no guarda score)
|
||||||
|
│ misma seed, un cambio plausible → judge.score = score_after │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼ GATE objetivo
|
||||||
|
comfyui_bump_skill_version(slug, change, score_before, score_after, judge_run_id=...)
|
||||||
|
│
|
||||||
|
score_after > score_before ?
|
||||||
|
├── sí → promueve: versions/vN.json (snapshot) + recipe_patch + semver↑ + growth_log
|
||||||
|
└── no → {ok:False} — NO se promueve (la variante se descarta)
|
||||||
|
```
|
||||||
|
|
||||||
|
Pasos concretos:
|
||||||
|
|
||||||
|
1. **Genera la canónica** con `judge=True`. El panel `comfyui-judge` emite un `score` y el pipeline
|
||||||
|
lo acumula en `score_mean`/`score_n` de la skill (vía `comfyui_update_skill_score`). Ese score es
|
||||||
|
el `score_before`.
|
||||||
|
2. **Genera una variante** con `recipe_patch` (p.ej. `{"params": {"steps": 32}}`) y la **misma seed**.
|
||||||
|
El patch se aplica en memoria, NO se guarda, y su score NO contamina la media. Su `judge.score` es
|
||||||
|
el `score_after`, y su `judge_run_id` es la evidencia.
|
||||||
|
3. **Promueve con el gate**: `comfyui_bump_skill_version` aplica el patch a `recipe.json`, sube el
|
||||||
|
semver y deja una línea en `growth_log.jsonl` **solo si `score_after > score_before`**. Si no
|
||||||
|
mejora, devuelve `{ok:False}` y la receta se queda como estaba. El gate es objetivo: lo decide el
|
||||||
|
número del juez, no quien lanza la generación.
|
||||||
|
|
||||||
|
Así `versions/` y `growth_log` reflejan **versiones de receta con mejora demostrada**, mientras
|
||||||
|
`score_mean` es la telemetría de calidad media de la versión vigente.
|
||||||
|
|
||||||
## Fronteras
|
## Fronteras
|
||||||
|
|
||||||
- **No genera ni descarga modelos**: una skill referencia checkpoints/LoRAs por nombre; deben
|
- **No genera ni descarga modelos**: una skill referencia checkpoints/LoRAs por nombre; deben
|
||||||
@@ -131,9 +175,13 @@ El paso "guardar la receta" se hace una sola vez; a partir de ahí cada generaci
|
|||||||
- **`blocks` soportados**: `facedetailer` y `hires_fix`. Otros post-procesos (IPAdapter,
|
- **`blocks` soportados**: `facedetailer` y `hires_fix`. Otros post-procesos (IPAdapter,
|
||||||
multi-ControlNet) se añaden creando su función-inyector hermana y registrándola en el dispatcher
|
multi-ControlNet) se añaden creando su función-inyector hermana y registrándola en el dispatcher
|
||||||
de `build_skill_workflow`.
|
de `build_skill_workflow`.
|
||||||
- **El scoring (`score_mean`/`score_n`) no se calcula aquí**: `ask_llm_vision` da el juicio del
|
- **El juicio (`comfyui-judge`) vive en su grupo**: este grupo lo *consume* (vía
|
||||||
modelo sobre una imagen, pero actualizar la receta con el score acumulado es trabajo de otra
|
`generate_with_skill_oneshot` con `judge=True`), pero el panel multi-juez —estético + CLIP +
|
||||||
pieza (el bucle de scoring) que reescribe la receta y la re-guarda con `comfyui_save_skill`.
|
LLM-vision— se documenta en [`comfyui-judge`](comfyui-judge.md). Aquí solo se acumula su `score`
|
||||||
|
en `score_mean` (`comfyui_update_skill_score`) y se usa como gate del bump.
|
||||||
|
- **El bump solo sube versiones, no genera ni juzga**: `comfyui_bump_skill_version` aplica el patch
|
||||||
|
y registra la mejora; generar la imagen y puntuarla es trabajo del pipeline + el panel-juez. Una
|
||||||
|
variante que no supera a la vigente se descarta sola (el gate la rechaza).
|
||||||
- **La librería es metadata local**: vive bajo `~/ComfyUI/skills_library` (no toca el venv ni los
|
- **La librería es metadata local**: vive bajo `~/ComfyUI/skills_library` (no toca el venv ni los
|
||||||
modelos en disco). No tiene repo propio ni se indexa — es estado vivo, como un `operations.db`.
|
modelos en disco). No tiene repo propio ni se indexa — es estado vivo, como un `operations.db`.
|
||||||
- **Las funciones impuras del grupo** (save/load/list, ask_llm_vision) no llevan unit tests por
|
- **Las funciones impuras del grupo** (save/load/list, ask_llm_vision) no llevan unit tests por
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
---
|
||||||
|
name: comfyui_bump_skill_version
|
||||||
|
kind: function
|
||||||
|
lang: py
|
||||||
|
domain: ml
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "def comfyui_bump_skill_version(slug: str, change: str, *, score_before: float, score_after: float, judge_run_id: str = None, recipe_patch: dict = None, force: bool = False, bump: str = \"minor\", library_dir: str = None, timestamp: float = None) -> dict"
|
||||||
|
description: "Promueve una version nueva de una skill ComfyUI SOLO si el score sube (gate objetivo del bucle de mejora, grupo comfyui-skill). Si score_after <= score_before y no force, rechaza con ok=False sin tocar nada. Si pasa: snapshot pre-mutacion versions/vN.json, aplica recipe_patch (deep-merge) a recipe.json, sube el semver (minor default), y appende a growth_log.jsonl una linea {version,date,change,score_before,score_after,judge_run_id,diff}. library_dir default ~/ComfyUI/skills_library. Slug inexistente -> ok=False. No usa datetime.now (deriva la fecha del timestamp recibido o time.time). Impura: disco. Nunca lanza."
|
||||||
|
error_type: error_py_core
|
||||||
|
tags: [comfyui, comfyui-skill, ml, skill, versioning, growth]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: []
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
imports: []
|
||||||
|
params:
|
||||||
|
- name: slug
|
||||||
|
desc: "Slug de la skill (su carpeta en la libreria)."
|
||||||
|
- name: change
|
||||||
|
desc: "Descripcion de una linea del cambio que motiva el bump (va al growth_log)."
|
||||||
|
- name: score_before
|
||||||
|
desc: "Score 0-10 de la version actual (del panel comfyui-judge). keyword-only."
|
||||||
|
- name: score_after
|
||||||
|
desc: "Score 0-10 de la variante candidata. Debe ser > score_before salvo force=True. keyword-only."
|
||||||
|
- name: judge_run_id
|
||||||
|
desc: "Identificador de la corrida del juez que justifica el bump (evidencia trazable). keyword-only."
|
||||||
|
- name: recipe_patch
|
||||||
|
desc: "Dict con los cambios a aplicar sobre la receta (deep-merge). Ej. {'params': {'steps': 32}}. keyword-only."
|
||||||
|
- name: force
|
||||||
|
desc: "Si True, salta el gate y promueve aunque el score no mejore. keyword-only."
|
||||||
|
- name: bump
|
||||||
|
desc: "Parte del semver a subir: 'minor' (default), 'major' o 'patch'. keyword-only."
|
||||||
|
- name: library_dir
|
||||||
|
desc: "Raiz de la libreria. Default ~/ComfyUI/skills_library. keyword-only."
|
||||||
|
- name: timestamp
|
||||||
|
desc: "Epoch en segundos para la fecha del growth_log; None = time.time(). keyword-only."
|
||||||
|
output: "dict {ok, slug, old_version, new_version, snapshot_file, growth_entry, recipe_path, error}. ok=False con error si el gate rechaza el bump, si la skill no existe, o si falla la escritura; nunca lanza."
|
||||||
|
tested: true
|
||||||
|
tests: [test_semver_helper, test_deep_merge_no_pisa_otras_claves, test_golden_promueve_cuando_score_sube, test_edge_major_y_patch, test_error_gate_bloquea_si_no_mejora, test_error_force_salta_gate, test_error_skill_inexistente]
|
||||||
|
test_file_path: "python/functions/ml/tests/test_comfyui_bump_skill_version.py"
|
||||||
|
file_path: "python/functions/ml/comfyui_bump_skill_version.py"
|
||||||
|
---
|
||||||
|
|
||||||
|
# comfyui_bump_skill_version
|
||||||
|
|
||||||
|
Pieza de cierre del bucle de mejora del grupo [`comfyui-skill`](../../../docs/capabilities/comfyui-skill.md):
|
||||||
|
una skill genera, el panel [`comfyui-judge`](../../../docs/capabilities/comfyui-judge.md) la
|
||||||
|
puntúa, y esta función promueve una versión nueva **solo si el score sube**. El juez decide, no
|
||||||
|
el humano. Es el "crecimiento por composición" del issue 0087 aplicado a la generación de imágenes.
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```python
|
||||||
|
import sys, os
|
||||||
|
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
|
||||||
|
from ml.comfyui_bump_skill_version import comfyui_bump_skill_version
|
||||||
|
|
||||||
|
# La variante con steps=32 puntuó 7.4 vs 6.5 de la versión vigente → se promueve.
|
||||||
|
res = comfyui_bump_skill_version(
|
||||||
|
"portrait_cinematic_sd15", "subir steps 28→32 (mejor detalle facial)",
|
||||||
|
score_before=6.5, score_after=7.4,
|
||||||
|
judge_run_id="judge_abc123", recipe_patch={"params": {"steps": 32}},
|
||||||
|
)
|
||||||
|
print(res["old_version"], "→", res["new_version"]) # 1.0.0 → 1.1.0
|
||||||
|
# recipe.json ahora en 1.1.0 con steps=32; versions/vN.json conserva la 1.0.0;
|
||||||
|
# growth_log.jsonl tiene una línea con score_before/after + judge_run_id.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cuando usarla
|
||||||
|
|
||||||
|
Tras juzgar una variante de una skill: si el `score` del panel supera al de la versión vigente,
|
||||||
|
llama a esta función para **promover** la mejora (snapshot + semver + growth_log). Si el score no
|
||||||
|
sube, NO la llames (o se rechaza por el gate) — esa es justo la garantía del bucle. Pásale el
|
||||||
|
`judge_run_id` de `comfyui_generate_with_skill_oneshot` como evidencia trazable.
|
||||||
|
|
||||||
|
## Gotchas
|
||||||
|
|
||||||
|
- **Gate duro**: `score_after <= score_before` sin `force=True` devuelve `{ok:False}` y NO toca
|
||||||
|
nada (ni snapshot, ni growth_log, ni versión). Es el comportamiento deseado, no un error.
|
||||||
|
- **`force=True`** salta el gate (promueve aunque empeore) y marca `growth_entry.forced=True`.
|
||||||
|
Úsalo solo para correcciones manuales, no en el bucle automático.
|
||||||
|
- **`recipe_patch` es deep-merge**: dicts anidados (`params`) se fusionan; listas (`loras`,
|
||||||
|
`blocks`) se reemplazan enteras (no se concatenan).
|
||||||
|
- **Snapshot pre-mutación**: `versions/vN.json` guarda la receta ANTES del patch; el `recipe.json`
|
||||||
|
queda con la versión nueva. Recupera la vieja con `comfyui_load_skill(slug, version=N)`.
|
||||||
|
- **Fecha sin `datetime.now()`**: se deriva de `timestamp` o `time.time()` vía `time.strftime`
|
||||||
|
(compatible con entornos que prohíben `datetime.now()`).
|
||||||
|
- **No genera ni juzga**: solo promueve la receta. Generar + puntuar es trabajo de
|
||||||
|
`comfyui_generate_with_skill_oneshot` + `comfyui_judge_image`.
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
"""comfyui_bump_skill_version — promueve una nueva versión de una *skill* ComfyUI.
|
||||||
|
|
||||||
|
Cierra el bucle de mejora del grupo `comfyui-skill`: una skill genera, el panel
|
||||||
|
`comfyui-judge` la puntúa y, **solo si el score sube**, esta función promociona una
|
||||||
|
versión nueva de la receta. Es la pieza de "crecimiento por composición" del issue 0087
|
||||||
|
aplicada a la generación de imágenes — el catálogo de recetas crece registrando mejoras
|
||||||
|
medibles, no inflando recetas a ciegas.
|
||||||
|
|
||||||
|
Qué hace, en orden:
|
||||||
|
|
||||||
|
1. **Gate objetivo**: si ``score_after <= score_before`` y no se pasa ``force=True``,
|
||||||
|
rechaza con ``{ok: False}`` sin tocar nada. El juez (no el humano) decide.
|
||||||
|
2. **Snapshot pre-mutación**: escribe ``versions/vN.json`` con la receta ACTUAL antes de
|
||||||
|
cambiarla (backup recuperable con ``comfyui_load_skill(slug, version=N)``).
|
||||||
|
3. **Aplica ``recipe_patch``** (deep-merge) sobre ``recipe.json``.
|
||||||
|
4. **Sube el semver** de la receta (``minor`` por defecto: 1.0.0 → 1.1.0).
|
||||||
|
5. **Append a ``growth_log.jsonl``**: una línea
|
||||||
|
``{version, date, change, score_before, score_after, judge_run_id, diff}``.
|
||||||
|
|
||||||
|
`library_dir` por defecto ``~/ComfyUI/skills_library``. Slug inexistente → ``{ok: False}``.
|
||||||
|
|
||||||
|
Impura: lee y escribe archivos en disco. Sin red. No usa ``datetime.now()`` (prohibido en
|
||||||
|
algunos entornos) — la fecha se deriva del ``timestamp`` recibido o de ``time.time()``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
DEFAULT_LIBRARY = "~/ComfyUI/skills_library"
|
||||||
|
|
||||||
|
|
||||||
|
def _lib_dir(library_dir):
|
||||||
|
return os.path.expanduser(library_dir or DEFAULT_LIBRARY)
|
||||||
|
|
||||||
|
|
||||||
|
def _bump_semver(version: str, part: str) -> str:
|
||||||
|
"""Sube un semver ``X.Y.Z`` por ``major``/``minor``/``patch``.
|
||||||
|
|
||||||
|
Tolerante a versiones mal formadas: si ``version`` no parsea como tres enteros,
|
||||||
|
parte de ``0.0.0`` antes de aplicar el incremento.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
nums = (list(map(int, str(version).split("."))) + [0, 0, 0])[:3]
|
||||||
|
major, minor, patch = nums
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
major, minor, patch = 0, 0, 0
|
||||||
|
if part == "major":
|
||||||
|
return f"{major + 1}.0.0"
|
||||||
|
if part == "patch":
|
||||||
|
return f"{major}.{minor}.{patch + 1}"
|
||||||
|
# minor (default)
|
||||||
|
return f"{major}.{minor + 1}.0"
|
||||||
|
|
||||||
|
|
||||||
|
def _deep_merge(base: dict, patch: dict) -> dict:
|
||||||
|
"""Merge recursivo de ``patch`` sobre ``base`` sin mutar los originales.
|
||||||
|
|
||||||
|
Las claves cuyo valor es dict en ambos se fusionan en profundidad; cualquier otro
|
||||||
|
tipo (incluida una lista, p.ej. ``loras``/``blocks``) se reemplaza entero.
|
||||||
|
"""
|
||||||
|
out = dict(base)
|
||||||
|
for key, val in (patch or {}).items():
|
||||||
|
if isinstance(val, dict) and isinstance(out.get(key), dict):
|
||||||
|
out[key] = _deep_merge(out[key], val)
|
||||||
|
else:
|
||||||
|
out[key] = val
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _next_version_index(versions_dir: str) -> int:
|
||||||
|
"""Siguiente N para ``versions/vN.json`` (1 + cuántos snapshots ya hay)."""
|
||||||
|
try:
|
||||||
|
existing = [f for f in os.listdir(versions_dir)
|
||||||
|
if f.startswith("v") and f.endswith(".json")]
|
||||||
|
except OSError:
|
||||||
|
existing = []
|
||||||
|
return len(existing) + 1
|
||||||
|
|
||||||
|
|
||||||
|
def comfyui_bump_skill_version(
|
||||||
|
slug: str,
|
||||||
|
change: str,
|
||||||
|
*,
|
||||||
|
score_before: float,
|
||||||
|
score_after: float,
|
||||||
|
judge_run_id: str = None,
|
||||||
|
recipe_patch: dict = None,
|
||||||
|
force: bool = False,
|
||||||
|
bump: str = "minor",
|
||||||
|
library_dir: str = None,
|
||||||
|
timestamp: float = None,
|
||||||
|
) -> dict:
|
||||||
|
"""Promueve una versión nueva de una skill si el score mejora (gate objetivo).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
slug: slug de la skill (su carpeta en la librería).
|
||||||
|
change: descripción de una línea del cambio que motiva el bump (va al growth_log).
|
||||||
|
score_before: score de la versión actual (del panel-juez). keyword-only.
|
||||||
|
score_after: score de la variante candidata. Debe ser ``> score_before`` salvo
|
||||||
|
``force=True``. keyword-only.
|
||||||
|
judge_run_id: identificador de la corrida del juez que justifica el bump
|
||||||
|
(evidencia trazable). keyword-only.
|
||||||
|
recipe_patch: dict con los cambios a aplicar sobre la receta (deep-merge). Por
|
||||||
|
ejemplo ``{"params": {"steps": 32}}``. keyword-only.
|
||||||
|
force: si True, salta el gate y promueve aunque el score no mejore. keyword-only.
|
||||||
|
bump: parte del semver a subir: ``minor`` (default), ``major`` o ``patch``.
|
||||||
|
keyword-only.
|
||||||
|
library_dir: raíz de la librería. Default ``~/ComfyUI/skills_library``. keyword-only.
|
||||||
|
timestamp: epoch en segundos para la fecha del growth_log; None = ``time.time()``.
|
||||||
|
keyword-only.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict ``{ok, slug, old_version, new_version, snapshot_file, growth_entry,
|
||||||
|
recipe_path, error}``. ``ok=False`` con ``error`` si el gate rechaza el bump, si
|
||||||
|
la skill no existe, o si falla la escritura; nunca lanza.
|
||||||
|
"""
|
||||||
|
if not slug or not isinstance(slug, str):
|
||||||
|
return {"ok": False, "slug": slug, "old_version": "", "new_version": "",
|
||||||
|
"snapshot_file": "", "growth_entry": None, "recipe_path": "",
|
||||||
|
"error": "slug requerido (string no vacío)"}
|
||||||
|
|
||||||
|
# 1. Gate objetivo: el juez decide, no el humano.
|
||||||
|
try:
|
||||||
|
sb = float(score_before)
|
||||||
|
sa = float(score_after)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return {"ok": False, "slug": slug, "old_version": "", "new_version": "",
|
||||||
|
"snapshot_file": "", "growth_entry": None, "recipe_path": "",
|
||||||
|
"error": f"score_before/score_after deben ser numéricos "
|
||||||
|
f"(recibido {score_before!r}, {score_after!r})"}
|
||||||
|
if sa <= sb and not force:
|
||||||
|
return {"ok": False, "slug": slug, "old_version": "", "new_version": "",
|
||||||
|
"snapshot_file": "", "growth_entry": None, "recipe_path": "",
|
||||||
|
"error": f"gate: score_after ({sa}) no supera score_before ({sb}); "
|
||||||
|
f"no se promueve (usa force=True para forzar)"}
|
||||||
|
|
||||||
|
lib = _lib_dir(library_dir)
|
||||||
|
skill_dir = os.path.join(lib, slug)
|
||||||
|
recipe_path = os.path.join(skill_dir, "recipe.json")
|
||||||
|
versions_dir = os.path.join(skill_dir, "versions")
|
||||||
|
|
||||||
|
if not os.path.isfile(recipe_path):
|
||||||
|
return {"ok": False, "slug": slug, "old_version": "", "new_version": "",
|
||||||
|
"snapshot_file": "", "growth_entry": None, "recipe_path": recipe_path,
|
||||||
|
"error": f"skill no encontrada: {slug!r} (sin recipe.json en {skill_dir})"}
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(recipe_path, encoding="utf-8") as fh:
|
||||||
|
recipe = json.load(fh)
|
||||||
|
except (OSError, json.JSONDecodeError) as exc:
|
||||||
|
return {"ok": False, "slug": slug, "old_version": "", "new_version": "",
|
||||||
|
"snapshot_file": "", "growth_entry": None, "recipe_path": recipe_path,
|
||||||
|
"error": f"no se pudo leer la receta: {exc}"}
|
||||||
|
|
||||||
|
old_version = recipe.get("version", "0.0.0")
|
||||||
|
new_version = _bump_semver(old_version, bump)
|
||||||
|
ts = float(timestamp) if timestamp is not None else time.time()
|
||||||
|
date = time.strftime("%Y-%m-%d", time.gmtime(ts))
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.makedirs(versions_dir, exist_ok=True)
|
||||||
|
# 2. Snapshot pre-mutación: preserva la receta actual antes de cambiarla.
|
||||||
|
n = _next_version_index(versions_dir)
|
||||||
|
snapshot_file = os.path.join(versions_dir, f"v{n}.json")
|
||||||
|
with open(snapshot_file, "w", encoding="utf-8") as fh:
|
||||||
|
json.dump(recipe, fh, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
# 3. Aplicar el patch (deep-merge) + 4. subir el semver.
|
||||||
|
new_recipe = _deep_merge(recipe, recipe_patch or {})
|
||||||
|
new_recipe["version"] = new_version
|
||||||
|
with open(recipe_path, "w", encoding="utf-8") as fh:
|
||||||
|
json.dump(new_recipe, fh, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
# 5. Bitácora append-only del crecimiento.
|
||||||
|
growth_entry = {
|
||||||
|
"version": new_version,
|
||||||
|
"date": date,
|
||||||
|
"ts": int(ts),
|
||||||
|
"change": change,
|
||||||
|
"score_before": sb,
|
||||||
|
"score_after": sa,
|
||||||
|
"judge_run_id": judge_run_id or "",
|
||||||
|
"diff": recipe_patch or {},
|
||||||
|
"forced": bool(force and sa <= sb),
|
||||||
|
}
|
||||||
|
with open(os.path.join(skill_dir, "growth_log.jsonl"), "a", encoding="utf-8") as fh:
|
||||||
|
fh.write(json.dumps(growth_entry, ensure_ascii=False) + "\n")
|
||||||
|
except OSError as exc:
|
||||||
|
return {"ok": False, "slug": slug, "old_version": old_version, "new_version": "",
|
||||||
|
"snapshot_file": "", "growth_entry": None, "recipe_path": recipe_path,
|
||||||
|
"error": f"fallo de escritura: {exc}"}
|
||||||
|
|
||||||
|
return {"ok": True, "slug": slug, "old_version": old_version,
|
||||||
|
"new_version": new_version, "snapshot_file": snapshot_file,
|
||||||
|
"growth_entry": growth_entry, "recipe_path": recipe_path, "error": ""}
|
||||||
|
|
||||||
|
|
||||||
|
# Alias con el nombre completo del ID para descubrimiento por convención.
|
||||||
|
bump_skill_version = comfyui_bump_skill_version
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Demo offline contra una librería temporal.
|
||||||
|
demo_lib = "/tmp/skills_bump_demo"
|
||||||
|
sdir = os.path.join(demo_lib, "demo_skill")
|
||||||
|
os.makedirs(os.path.join(sdir, "versions"), exist_ok=True)
|
||||||
|
with open(os.path.join(sdir, "recipe.json"), "w", encoding="utf-8") as f:
|
||||||
|
json.dump({"schema_version": 1, "slug": "demo_skill", "version": "1.0.0",
|
||||||
|
"base_workflow": "txt2img", "params": {"steps": 28}}, f, indent=2)
|
||||||
|
|
||||||
|
# Gate bloquea cuando no mejora.
|
||||||
|
blocked = comfyui_bump_skill_version("demo_skill", "subir steps", score_before=7.0,
|
||||||
|
score_after=6.5, library_dir=demo_lib)
|
||||||
|
print("gate bloquea:", blocked["ok"], "->", blocked["error"], file=sys.stderr)
|
||||||
|
|
||||||
|
# Promueve cuando mejora.
|
||||||
|
ok = comfyui_bump_skill_version("demo_skill", "subir steps a 32", score_before=6.5,
|
||||||
|
score_after=7.4, judge_run_id="judge_abc",
|
||||||
|
recipe_patch={"params": {"steps": 32}},
|
||||||
|
library_dir=demo_lib)
|
||||||
|
print(json.dumps(ok, indent=2, ensure_ascii=False))
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
name: comfyui_update_skill_score
|
||||||
|
kind: function
|
||||||
|
lang: py
|
||||||
|
domain: ml
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "def comfyui_update_skill_score(slug: str, new_score: float, *, library_dir: str = None) -> dict"
|
||||||
|
description: "Acumula el score de un juicio en la media de una skill ComfyUI (score_mean/score_n) por media incremental, reescribiendo recipe.json en sitio. A diferencia de comfyui_save_skill NO crea snapshot versions/vN.json ni entrada en growth_log: el score es telemetria de la version vigente, no una version nueva (los snapshots los reserva comfyui_bump_skill_version). library_dir default ~/ComfyUI/skills_library. Slug inexistente o score no numerico -> ok=False. Impura: disco. Nunca lanza."
|
||||||
|
error_type: error_py_core
|
||||||
|
tags: [comfyui, comfyui-skill, ml, skill, scoring]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: []
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
imports: []
|
||||||
|
params:
|
||||||
|
- name: slug
|
||||||
|
desc: "Slug de la skill (su carpeta en la libreria)."
|
||||||
|
- name: new_score
|
||||||
|
desc: "Score 0-10 del ultimo juicio (p.ej. comfyui_judge_image(...)['score'])."
|
||||||
|
- name: library_dir
|
||||||
|
desc: "Raiz de la libreria. Default ~/ComfyUI/skills_library. keyword-only."
|
||||||
|
output: "dict {ok, slug, score_mean, score_n, prev_score_mean, prev_score_n, error}. En exito score_mean/score_n son los valores actualizados. Slug inexistente / score no numerico / fallo de escritura -> ok=False; nunca lanza."
|
||||||
|
tested: true
|
||||||
|
tests: [test_golden_media_incremental, test_edge_sin_campos_previos, test_error_skill_inexistente, test_error_score_no_numerico]
|
||||||
|
test_file_path: "python/functions/ml/tests/test_comfyui_update_skill_score.py"
|
||||||
|
file_path: "python/functions/ml/comfyui_update_skill_score.py"
|
||||||
|
---
|
||||||
|
|
||||||
|
# comfyui_update_skill_score
|
||||||
|
|
||||||
|
Acumulador de telemetría del grupo [`comfyui-skill`](../../../docs/capabilities/comfyui-skill.md).
|
||||||
|
Cada juicio del panel [`comfyui-judge`](../../../docs/capabilities/comfyui-judge.md) sobre una
|
||||||
|
imagen de la skill se incorpora a la media `score_mean`/`score_n` de la receta, reescribiendo
|
||||||
|
`recipe.json` en sitio. Es lo que el pipeline `comfyui_generate_with_skill_oneshot` llama por dentro.
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```python
|
||||||
|
import sys, os
|
||||||
|
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
|
||||||
|
from ml.comfyui_update_skill_score import comfyui_update_skill_score
|
||||||
|
|
||||||
|
# Tras juzgar una imagen de la skill con score 7.4:
|
||||||
|
res = comfyui_update_skill_score("portrait_cinematic_sd15", 7.4)
|
||||||
|
print(res["score_mean"], res["score_n"]) # media acumulada y nº de juicios
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cuando usarla
|
||||||
|
|
||||||
|
Después de puntuar una imagen generada por una skill (su receta canónica, sin variantes ad-hoc),
|
||||||
|
para que `score_mean` refleje la calidad media observada. Normalmente NO se llama directo: lo hace
|
||||||
|
`comfyui_generate_with_skill_oneshot` cuando `judge=True` y no hay `recipe_patch`. Llámala a mano
|
||||||
|
solo si juzgas por separado.
|
||||||
|
|
||||||
|
## Gotchas
|
||||||
|
|
||||||
|
- **In-place, sin snapshot**: reescribe `recipe.json` y nada más. No toca `versions/` ni
|
||||||
|
`growth_log` — eso lo reserva `comfyui_bump_skill_version` para versiones reales.
|
||||||
|
- **Media incremental** `mean_k = mean_{k-1} + (x - mean_{k-1})/k`: estable numéricamente, no
|
||||||
|
necesita re-leer el histórico. `score_mean` se redondea a 4 decimales.
|
||||||
|
- **No acumules variantes**: si juzgas una variante de prueba (un `recipe_patch` no guardado), NO
|
||||||
|
la metas en la media de la skill canónica — contaminaría `score_mean`. El pipeline ya lo evita.
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
"""comfyui_update_skill_score — acumula el score de un juicio en una *skill* ComfyUI.
|
||||||
|
|
||||||
|
Cada vez que el panel `comfyui-judge` puntúa una imagen generada por una skill, esta
|
||||||
|
función incorpora ese score a la media acumulada de la receta (``score_mean`` / ``score_n``)
|
||||||
|
mediante **media incremental**, reescribiendo ``recipe.json`` en sitio.
|
||||||
|
|
||||||
|
A diferencia de `comfyui_save_skill`, NO crea un snapshot ``versions/vN.json`` ni añade
|
||||||
|
entrada al ``growth_log``: el score es telemetría acumulada de la versión vigente, no una
|
||||||
|
versión nueva. Los snapshots y el growth_log los reserva `comfyui_bump_skill_version` para
|
||||||
|
promociones reales (cuando el score mejora y se sube el semver). Así ``versions/`` refleja
|
||||||
|
versiones de receta y no se ensucia con una entrada por cada generación.
|
||||||
|
|
||||||
|
`library_dir` por defecto ``~/ComfyUI/skills_library``. Slug inexistente → ``{ok: False}``.
|
||||||
|
|
||||||
|
Impura: lee y reescribe ``recipe.json`` en disco. Sin red.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
DEFAULT_LIBRARY = "~/ComfyUI/skills_library"
|
||||||
|
|
||||||
|
|
||||||
|
def _lib_dir(library_dir):
|
||||||
|
return os.path.expanduser(library_dir or DEFAULT_LIBRARY)
|
||||||
|
|
||||||
|
|
||||||
|
def comfyui_update_skill_score(
|
||||||
|
slug: str,
|
||||||
|
new_score: float,
|
||||||
|
*,
|
||||||
|
library_dir: str = None,
|
||||||
|
) -> dict:
|
||||||
|
"""Acumula ``new_score`` en la media de la skill (media incremental, in-place).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
slug: slug de la skill (su carpeta en la librería).
|
||||||
|
new_score: score 0-10 del último juicio (p.ej. ``comfyui_judge_image(...)["score"]``).
|
||||||
|
library_dir: raíz de la librería. Default ``~/ComfyUI/skills_library``. keyword-only.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict ``{ok, slug, score_mean, score_n, prev_score_mean, prev_score_n, error}``.
|
||||||
|
En éxito ``ok=True`` y ``score_mean``/``score_n`` son los valores actualizados. Si
|
||||||
|
la skill no existe, el score no es numérico, o falla la escritura, ``ok=False`` con
|
||||||
|
``error``; nunca lanza.
|
||||||
|
"""
|
||||||
|
if not slug or not isinstance(slug, str):
|
||||||
|
return {"ok": False, "slug": slug, "score_mean": 0.0, "score_n": 0,
|
||||||
|
"prev_score_mean": 0.0, "prev_score_n": 0,
|
||||||
|
"error": "slug requerido (string no vacío)"}
|
||||||
|
try:
|
||||||
|
score = float(new_score)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return {"ok": False, "slug": slug, "score_mean": 0.0, "score_n": 0,
|
||||||
|
"prev_score_mean": 0.0, "prev_score_n": 0,
|
||||||
|
"error": f"new_score debe ser numérico (recibido {new_score!r})"}
|
||||||
|
|
||||||
|
lib = _lib_dir(library_dir)
|
||||||
|
recipe_path = os.path.join(lib, slug, "recipe.json")
|
||||||
|
if not os.path.isfile(recipe_path):
|
||||||
|
return {"ok": False, "slug": slug, "score_mean": 0.0, "score_n": 0,
|
||||||
|
"prev_score_mean": 0.0, "prev_score_n": 0,
|
||||||
|
"error": f"skill no encontrada: {slug!r} (sin recipe.json)"}
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(recipe_path, encoding="utf-8") as fh:
|
||||||
|
recipe = json.load(fh)
|
||||||
|
except (OSError, json.JSONDecodeError) as exc:
|
||||||
|
return {"ok": False, "slug": slug, "score_mean": 0.0, "score_n": 0,
|
||||||
|
"prev_score_mean": 0.0, "prev_score_n": 0,
|
||||||
|
"error": f"no se pudo leer la receta: {exc}"}
|
||||||
|
|
||||||
|
try:
|
||||||
|
prev_mean = float(recipe.get("score_mean", 0.0) or 0.0)
|
||||||
|
prev_n = int(recipe.get("score_n", 0) or 0)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
prev_mean, prev_n = 0.0, 0
|
||||||
|
|
||||||
|
new_n = prev_n + 1
|
||||||
|
# Media incremental: mean_k = mean_{k-1} + (x_k - mean_{k-1}) / k.
|
||||||
|
new_mean = prev_mean + (score - prev_mean) / new_n
|
||||||
|
recipe["score_mean"] = round(new_mean, 4)
|
||||||
|
recipe["score_n"] = new_n
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(recipe_path, "w", encoding="utf-8") as fh:
|
||||||
|
json.dump(recipe, fh, indent=2, ensure_ascii=False)
|
||||||
|
except OSError as exc:
|
||||||
|
return {"ok": False, "slug": slug, "score_mean": prev_mean, "score_n": prev_n,
|
||||||
|
"prev_score_mean": prev_mean, "prev_score_n": prev_n,
|
||||||
|
"error": f"fallo de escritura: {exc}"}
|
||||||
|
|
||||||
|
return {"ok": True, "slug": slug, "score_mean": recipe["score_mean"],
|
||||||
|
"score_n": new_n, "prev_score_mean": prev_mean, "prev_score_n": prev_n,
|
||||||
|
"error": ""}
|
||||||
|
|
||||||
|
|
||||||
|
# Alias con el nombre completo del ID para descubrimiento por convención.
|
||||||
|
update_skill_score = comfyui_update_skill_score
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import sys
|
||||||
|
|
||||||
|
demo_lib = "/tmp/skills_score_demo"
|
||||||
|
sdir = os.path.join(demo_lib, "demo_skill")
|
||||||
|
os.makedirs(sdir, exist_ok=True)
|
||||||
|
with open(os.path.join(sdir, "recipe.json"), "w", encoding="utf-8") as f:
|
||||||
|
json.dump({"schema_version": 1, "slug": "demo_skill", "version": "1.0.0",
|
||||||
|
"base_workflow": "txt2img", "score_mean": 0.0, "score_n": 0}, f, indent=2)
|
||||||
|
|
||||||
|
for s in (7.0, 8.0, 6.0):
|
||||||
|
res = comfyui_update_skill_score("demo_skill", s, library_dir=demo_lib)
|
||||||
|
print(f"score={s} -> mean={res['score_mean']} n={res['score_n']}", file=sys.stderr)
|
||||||
|
print(json.dumps(res, indent=2, ensure_ascii=False))
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
"""Tests offline para comfyui_bump_skill_version (impura, sin red/GPU).
|
||||||
|
|
||||||
|
Verifican el contrato del bucle de mejora contra una librería temporal:
|
||||||
|
- golden: score sube → promueve, snapshot pre-mutación, semver subido, recipe_patch aplicado,
|
||||||
|
growth_log con una línea bien formada,
|
||||||
|
- edge: bump major/patch + deep-merge anidado sin pisar otras claves,
|
||||||
|
- error: gate (score no mejora sin force) → ok=False; force salta el gate; skill inexistente.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(__file__))
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||||
|
|
||||||
|
import pytest # noqa: E402
|
||||||
|
|
||||||
|
from ml.comfyui_bump_skill_version import comfyui_bump_skill_version, _bump_semver, _deep_merge # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def _seed_skill(lib, slug="demo_skill", version="1.0.0", **extra):
|
||||||
|
sdir = os.path.join(lib, slug)
|
||||||
|
os.makedirs(os.path.join(sdir, "versions"), exist_ok=True)
|
||||||
|
recipe = {"schema_version": 1, "slug": slug, "version": version,
|
||||||
|
"base_workflow": "txt2img", "checkpoint": "dreamshaper_8.safetensors",
|
||||||
|
"params": {"steps": 28, "cfg": 6.0}, **extra}
|
||||||
|
with open(os.path.join(sdir, "recipe.json"), "w", encoding="utf-8") as f:
|
||||||
|
json.dump(recipe, f)
|
||||||
|
return sdir
|
||||||
|
|
||||||
|
|
||||||
|
def test_semver_helper():
|
||||||
|
assert _bump_semver("1.0.0", "minor") == "1.1.0"
|
||||||
|
assert _bump_semver("1.2.3", "major") == "2.0.0"
|
||||||
|
assert _bump_semver("1.2.3", "patch") == "1.2.4"
|
||||||
|
assert _bump_semver("nan", "minor") == "0.1.0" # versión mal formada → 0.0.0 base
|
||||||
|
|
||||||
|
|
||||||
|
def test_deep_merge_no_pisa_otras_claves():
|
||||||
|
base = {"params": {"steps": 28, "cfg": 6.0}, "checkpoint": "a"}
|
||||||
|
out = _deep_merge(base, {"params": {"steps": 32}})
|
||||||
|
assert out == {"params": {"steps": 32, "cfg": 6.0}, "checkpoint": "a"}
|
||||||
|
assert base["params"]["steps"] == 28 # no muta el original
|
||||||
|
|
||||||
|
|
||||||
|
def test_golden_promueve_cuando_score_sube(tmp_path):
|
||||||
|
lib = str(tmp_path)
|
||||||
|
_seed_skill(lib)
|
||||||
|
res = comfyui_bump_skill_version(
|
||||||
|
"demo_skill", "subir steps a 32", score_before=6.5, score_after=7.4,
|
||||||
|
judge_run_id="judge_xyz", recipe_patch={"params": {"steps": 32}}, library_dir=lib)
|
||||||
|
|
||||||
|
assert res["ok"] is True
|
||||||
|
assert res["old_version"] == "1.0.0"
|
||||||
|
assert res["new_version"] == "1.1.0"
|
||||||
|
|
||||||
|
# snapshot pre-mutación conserva la receta vieja (steps 28).
|
||||||
|
with open(res["snapshot_file"], encoding="utf-8") as f:
|
||||||
|
snap = json.load(f)
|
||||||
|
assert snap["params"]["steps"] == 28
|
||||||
|
assert snap["version"] == "1.0.0"
|
||||||
|
|
||||||
|
# recipe.json mutado: patch aplicado + semver subido, cfg intacto.
|
||||||
|
with open(os.path.join(lib, "demo_skill", "recipe.json"), encoding="utf-8") as f:
|
||||||
|
cur = json.load(f)
|
||||||
|
assert cur["params"]["steps"] == 32
|
||||||
|
assert cur["params"]["cfg"] == 6.0
|
||||||
|
assert cur["version"] == "1.1.0"
|
||||||
|
|
||||||
|
# growth_log: una línea con la evidencia.
|
||||||
|
with open(os.path.join(lib, "demo_skill", "growth_log.jsonl"), encoding="utf-8") as f:
|
||||||
|
lines = [json.loads(x) for x in f if x.strip()]
|
||||||
|
assert len(lines) == 1
|
||||||
|
entry = lines[0]
|
||||||
|
assert entry["version"] == "1.1.0"
|
||||||
|
assert entry["score_before"] == 6.5
|
||||||
|
assert entry["score_after"] == 7.4
|
||||||
|
assert entry["judge_run_id"] == "judge_xyz"
|
||||||
|
assert entry["diff"] == {"params": {"steps": 32}}
|
||||||
|
|
||||||
|
|
||||||
|
def test_edge_major_y_patch(tmp_path):
|
||||||
|
lib = str(tmp_path)
|
||||||
|
_seed_skill(lib, slug="s_major", version="1.4.2")
|
||||||
|
r1 = comfyui_bump_skill_version("s_major", "rework", score_before=5.0, score_after=8.0,
|
||||||
|
bump="major", library_dir=lib)
|
||||||
|
assert r1["new_version"] == "2.0.0"
|
||||||
|
|
||||||
|
_seed_skill(lib, slug="s_patch", version="1.4.2")
|
||||||
|
r2 = comfyui_bump_skill_version("s_patch", "tweak", score_before=5.0, score_after=5.1,
|
||||||
|
bump="patch", library_dir=lib)
|
||||||
|
assert r2["new_version"] == "1.4.3"
|
||||||
|
|
||||||
|
|
||||||
|
def test_error_gate_bloquea_si_no_mejora(tmp_path):
|
||||||
|
lib = str(tmp_path)
|
||||||
|
sdir = _seed_skill(lib)
|
||||||
|
res = comfyui_bump_skill_version("demo_skill", "no mejora", score_before=7.0,
|
||||||
|
score_after=6.5, library_dir=lib)
|
||||||
|
assert res["ok"] is False
|
||||||
|
assert "gate" in res["error"]
|
||||||
|
# No se tocó nada: ni snapshot ni growth_log ni cambio de versión.
|
||||||
|
assert not os.path.exists(os.path.join(sdir, "growth_log.jsonl"))
|
||||||
|
with open(os.path.join(sdir, "recipe.json"), encoding="utf-8") as f:
|
||||||
|
assert json.load(f)["version"] == "1.0.0"
|
||||||
|
|
||||||
|
|
||||||
|
def test_error_force_salta_gate(tmp_path):
|
||||||
|
lib = str(tmp_path)
|
||||||
|
_seed_skill(lib)
|
||||||
|
res = comfyui_bump_skill_version("demo_skill", "forzado", score_before=7.0,
|
||||||
|
score_after=6.5, force=True, library_dir=lib)
|
||||||
|
assert res["ok"] is True
|
||||||
|
assert res["new_version"] == "1.1.0"
|
||||||
|
assert res["growth_entry"]["forced"] is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_error_skill_inexistente(tmp_path):
|
||||||
|
res = comfyui_bump_skill_version("no_existe", "x", score_before=1.0, score_after=2.0,
|
||||||
|
library_dir=str(tmp_path))
|
||||||
|
assert res["ok"] is False
|
||||||
|
assert "no encontrada" in res["error"]
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(pytest.main([__file__, "-q"]))
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
"""Tests offline para comfyui_update_skill_score (impura, sin red).
|
||||||
|
|
||||||
|
- golden: media incremental correcta tras varios juicios; reescribe in-place sin crear
|
||||||
|
snapshots ni growth_log,
|
||||||
|
- edge: arranca desde score_mean/score_n ausentes (skill recién creada),
|
||||||
|
- error: skill inexistente / score no numérico → ok=False.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(__file__))
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||||
|
|
||||||
|
import pytest # noqa: E402
|
||||||
|
|
||||||
|
from ml.comfyui_update_skill_score import comfyui_update_skill_score # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def _seed(lib, slug="demo", **extra):
|
||||||
|
sdir = os.path.join(lib, slug)
|
||||||
|
os.makedirs(sdir, exist_ok=True)
|
||||||
|
recipe = {"schema_version": 1, "slug": slug, "version": "1.0.0",
|
||||||
|
"base_workflow": "txt2img", **extra}
|
||||||
|
with open(os.path.join(sdir, "recipe.json"), "w", encoding="utf-8") as f:
|
||||||
|
json.dump(recipe, f)
|
||||||
|
return sdir
|
||||||
|
|
||||||
|
|
||||||
|
def test_golden_media_incremental(tmp_path):
|
||||||
|
lib = str(tmp_path)
|
||||||
|
sdir = _seed(lib, score_mean=0.0, score_n=0)
|
||||||
|
|
||||||
|
r1 = comfyui_update_skill_score("demo", 7.0, library_dir=lib)
|
||||||
|
assert r1["ok"] and r1["score_n"] == 1 and r1["score_mean"] == 7.0
|
||||||
|
|
||||||
|
r2 = comfyui_update_skill_score("demo", 8.0, library_dir=lib)
|
||||||
|
assert r2["score_n"] == 2 and r2["score_mean"] == 7.5
|
||||||
|
|
||||||
|
r3 = comfyui_update_skill_score("demo", 6.0, library_dir=lib)
|
||||||
|
assert r3["score_n"] == 3
|
||||||
|
assert abs(r3["score_mean"] - 7.0) < 1e-9 # (7+8+6)/3
|
||||||
|
|
||||||
|
# in-place: ni versions/ ni growth_log creados por este helper.
|
||||||
|
assert not os.path.exists(os.path.join(sdir, "versions"))
|
||||||
|
assert not os.path.exists(os.path.join(sdir, "growth_log.jsonl"))
|
||||||
|
|
||||||
|
with open(os.path.join(sdir, "recipe.json"), encoding="utf-8") as f:
|
||||||
|
cur = json.load(f)
|
||||||
|
assert cur["score_n"] == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_edge_sin_campos_previos(tmp_path):
|
||||||
|
lib = str(tmp_path)
|
||||||
|
_seed(lib, slug="fresh") # sin score_mean/score_n
|
||||||
|
res = comfyui_update_skill_score("fresh", 9.0, library_dir=lib)
|
||||||
|
assert res["ok"] and res["score_mean"] == 9.0 and res["score_n"] == 1
|
||||||
|
assert res["prev_score_n"] == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_error_skill_inexistente(tmp_path):
|
||||||
|
res = comfyui_update_skill_score("nope", 5.0, library_dir=str(tmp_path))
|
||||||
|
assert res["ok"] is False and "no encontrada" in res["error"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_error_score_no_numerico(tmp_path):
|
||||||
|
lib = str(tmp_path)
|
||||||
|
_seed(lib, slug="x", score_mean=0.0, score_n=0)
|
||||||
|
res = comfyui_update_skill_score("x", "siete", library_dir=lib)
|
||||||
|
assert res["ok"] is False and "numérico" in res["error"]
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(pytest.main([__file__, "-q"]))
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
---
|
||||||
|
name: comfyui_generate_with_skill_oneshot
|
||||||
|
kind: pipeline
|
||||||
|
lang: py
|
||||||
|
domain: pipelines
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "def comfyui_generate_with_skill_oneshot(slug: str, subject: str, *, server: str = \"127.0.0.1:8188\", dest: str | None = None, seed: int = 0, judge: bool = True, recipe_patch: dict | None = None, library_dir: str | None = None, wait_timeout: float = 600.0) -> dict"
|
||||||
|
description: "Pipeline skill + subject -> PNG juzgado en una sola llamada. Carga la receta de la skill, la compila para el subject (build_skill_workflow), encola, espera, descarga el PNG y si judge=True lo puntua con el panel comfyui-judge, acumulando el score en la media de la skill (salvo que se pase recipe_patch, que prueba una variante en memoria sin guardar score). Cierra el bucle de generacion->juicio del grupo comfyui-skill (issue 0087). Compone comfyui_load_skill + comfyui_build_skill_workflow + comfyui_submit_workflow + comfyui_wait_result + comfyui_fetch_output_image + comfyui_judge_image + comfyui_update_skill_score. Impuro: HTTP + disco + API Anthropic."
|
||||||
|
tags: [comfyui, comfyui-skill, pipelines, txt2img, judge, launcher]
|
||||||
|
uses_functions: [comfyui_load_skill_py_ml, comfyui_build_skill_workflow_py_ml, comfyui_submit_workflow_py_ml, comfyui_wait_result_py_ml, comfyui_fetch_output_image_py_ml, comfyui_judge_image_py_ml, comfyui_update_skill_score_py_ml]
|
||||||
|
uses_types: []
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: error_py_core
|
||||||
|
imports: [comfyui_load_skill_py_ml, comfyui_build_skill_workflow_py_ml, comfyui_submit_workflow_py_ml, comfyui_wait_result_py_ml, comfyui_fetch_output_image_py_ml, comfyui_judge_image_py_ml, comfyui_update_skill_score_py_ml]
|
||||||
|
params:
|
||||||
|
- name: slug
|
||||||
|
desc: "Slug de la skill a usar (su carpeta en la libreria)."
|
||||||
|
- name: subject
|
||||||
|
desc: "Sujeto concreto que sustituye {subject} en el scaffold del prompt (p.ej. 'a woman with red hair')."
|
||||||
|
- name: server
|
||||||
|
desc: "host:port del servidor ComfyUI (sin esquema). keyword-only."
|
||||||
|
- name: dest
|
||||||
|
desc: "Directorio local donde guardar el PNG (None = cwd). keyword-only."
|
||||||
|
- name: seed
|
||||||
|
desc: "Semilla de generacion. keyword-only."
|
||||||
|
- name: judge
|
||||||
|
desc: "Si True, puntua el PNG con el panel comfyui-judge y acumula el score en la skill (salvo recipe_patch). keyword-only."
|
||||||
|
- name: recipe_patch
|
||||||
|
desc: "Dict de cambios aplicados a la receta EN MEMORIA antes de compilar (deep-merge), para probar una variante sin guardarla; con patch el score NO se acumula. keyword-only."
|
||||||
|
- name: library_dir
|
||||||
|
desc: "Raiz de la libreria. Default ~/ComfyUI/skills_library. keyword-only."
|
||||||
|
- name: wait_timeout
|
||||||
|
desc: "Segundos maximos esperando al servidor. keyword-only."
|
||||||
|
output: "dict {ok, slug, prompt_id, image_path, prompt_resolved, judge, judge_run_id, score_mean, score_n, error}. judge = {verdict, score, votes} o None. judge_run_id correlaciona el juicio (evidencia para comfyui_bump_skill_version). Si falla, ok=False y error explica el paso."
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "python/functions/pipelines/comfyui_generate_with_skill_oneshot.py"
|
||||||
|
---
|
||||||
|
|
||||||
|
# comfyui_generate_with_skill_oneshot
|
||||||
|
|
||||||
|
One-shot del bucle de mejora del grupo [`comfyui-skill`](../../../docs/capabilities/comfyui-skill.md):
|
||||||
|
de una skill guardada a un PNG **ya puntuado** por el panel
|
||||||
|
[`comfyui-judge`](../../../docs/capabilities/comfyui-judge.md), en una llamada. Promoción de la
|
||||||
|
secuencia repetida `load → build → submit → wait → fetch → judge` (issue 0087).
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```python
|
||||||
|
import sys, os
|
||||||
|
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
|
||||||
|
from pipelines.comfyui_generate_with_skill_oneshot import comfyui_generate_with_skill_oneshot
|
||||||
|
|
||||||
|
# Genera + juzga con la receta canónica (acumula score_mean):
|
||||||
|
res = comfyui_generate_with_skill_oneshot(
|
||||||
|
"portrait_cinematic_sd15", "a woman with red hair",
|
||||||
|
dest="/tmp/comfy_skill", seed=42, judge=True,
|
||||||
|
)
|
||||||
|
print(res["image_path"], res["judge"]["verdict"], res["judge"]["score"], res["score_mean"])
|
||||||
|
|
||||||
|
# Probar una variante sin guardar (no toca score_mean); su judge_run_id sirve de evidencia
|
||||||
|
# para promoverla con comfyui_bump_skill_version si supera a la canónica:
|
||||||
|
var = comfyui_generate_with_skill_oneshot(
|
||||||
|
"portrait_cinematic_sd15", "a woman with red hair",
|
||||||
|
seed=42, recipe_patch={"params": {"steps": 32}},
|
||||||
|
)
|
||||||
|
print(var["judge"]["score"], var["judge_run_id"])
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cuando usarla
|
||||||
|
|
||||||
|
Cuando quieras generar una imagen a partir de una skill ya guardada y saber al instante si es
|
||||||
|
buena (veredicto del panel). Para el **bucle de mejora**: genera la canónica, luego genera una
|
||||||
|
variante con `recipe_patch`, compara `judge.score`, y si la variante gana llama a
|
||||||
|
`comfyui_bump_skill_version` con su `judge_run_id`.
|
||||||
|
|
||||||
|
## Gotchas
|
||||||
|
|
||||||
|
- **Cuesta API**: con `judge=True` invoca el panel multi-juez (incluye 1 llamada LLM-vision a
|
||||||
|
Anthropic). Pásalo a `judge=False` para generar sin puntuar.
|
||||||
|
- **`recipe_patch` no persiste**: aplica la variante solo en memoria y NO acumula su score en la
|
||||||
|
skill (es una prueba). Promueve la variante explícitamente con `comfyui_bump_skill_version`.
|
||||||
|
- **Server vivo requerido**: necesita ComfyUI en `server` con el checkpoint/LoRAs/detector de la
|
||||||
|
receta instalados. `build_skill_workflow` es puro y no valida contra el servidor.
|
||||||
|
- **`wait_timeout` amplio (600s)**: cubre jobs con facedetailer/hires. Imágenes simples retornan en
|
||||||
|
cuanto los outputs están listos.
|
||||||
|
- **Juez caído ≠ fallo fatal**: si el panel entero falla, la imagen ya está en disco; el resultado
|
||||||
|
vuelve con `ok=True`, `judge=None` y `error` anotado.
|
||||||
|
- **OOM en 8GB**: si el server va corto de VRAM, usa una skill SD1.5 (`dreamshaper_8`) a resolución
|
||||||
|
modesta; el error de OOM se propaga en `{ok:False, error}` sin reiniciar el server.
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
"""comfyui_generate_with_skill_oneshot — skill + subject → PNG juzgado en una llamada.
|
||||||
|
|
||||||
|
Cierra el bucle "habilidades de generación cada vez mejores" del grupo `comfyui-skill`:
|
||||||
|
carga una receta de skill, la compila para un *subject* concreto, encola, espera, descarga
|
||||||
|
el PNG y (si ``judge=True``) lo puntúa con el panel `comfyui-judge`, acumulando el score en
|
||||||
|
la media de la skill. Es la promoción a one-shot de la secuencia repetida
|
||||||
|
load → build → submit → wait → fetch → judge (issue 0087).
|
||||||
|
|
||||||
|
Compone funciones del registry:
|
||||||
|
|
||||||
|
comfyui_load_skill_py_ml (lee recipe.json)
|
||||||
|
comfyui_build_skill_workflow_py_ml (receta + subject → workflow API format, PURA)
|
||||||
|
comfyui_submit_workflow_py_ml (POST /prompt)
|
||||||
|
comfyui_wait_result_py_ml (poll /history)
|
||||||
|
comfyui_fetch_output_image_py_ml (GET /view → disco)
|
||||||
|
comfyui_judge_image_py_ml (panel multi-juez: estético + CLIP + LLM-vision)
|
||||||
|
comfyui_update_skill_score_py_ml (media incremental de score_mean/score_n)
|
||||||
|
|
||||||
|
Pipeline impuro: red (HTTP) + escritura en disco + (si juzga) API Anthropic.
|
||||||
|
|
||||||
|
`recipe_patch` permite probar una **variante** en memoria sin guardarla: se aplica a la
|
||||||
|
receta cargada antes de compilar. Cuando se pasa un patch, el score juzgado NO se acumula en
|
||||||
|
``score_mean`` (es una prueba, no la receta canónica): el caller decide si promover la
|
||||||
|
variante con `comfyui_bump_skill_version`. Sin patch, el score sí alimenta la media de la skill.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Importa las funciones del registry (mismo árbol python/functions).
|
||||||
|
_FUNCTIONS_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
if _FUNCTIONS_ROOT not in sys.path:
|
||||||
|
sys.path.insert(0, _FUNCTIONS_ROOT)
|
||||||
|
|
||||||
|
from ml.comfyui_build_skill_workflow import build_skill_workflow, SkillWorkflowError
|
||||||
|
from ml.comfyui_fetch_output_image import comfyui_fetch_output_image
|
||||||
|
from ml.comfyui_judge_image import comfyui_judge_image
|
||||||
|
from ml.comfyui_load_skill import comfyui_load_skill
|
||||||
|
from ml.comfyui_submit_workflow import comfyui_submit_workflow
|
||||||
|
from ml.comfyui_update_skill_score import comfyui_update_skill_score
|
||||||
|
from ml.comfyui_wait_result import comfyui_wait_result
|
||||||
|
|
||||||
|
|
||||||
|
def _deep_merge(base: dict, patch: dict) -> dict:
|
||||||
|
"""Merge recursivo de ``patch`` sobre ``base`` sin mutar los originales."""
|
||||||
|
out = dict(base)
|
||||||
|
for key, val in (patch or {}).items():
|
||||||
|
if isinstance(val, dict) and isinstance(out.get(key), dict):
|
||||||
|
out[key] = _deep_merge(out[key], val)
|
||||||
|
else:
|
||||||
|
out[key] = val
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_prompt(recipe: dict, subject: str) -> str:
|
||||||
|
"""Reconstruye el prompt positivo resuelto (para pasárselo al juez de fidelidad)."""
|
||||||
|
scaffold = recipe.get("prompt_scaffold") or {}
|
||||||
|
positive = str(scaffold.get("positive", "") or "")
|
||||||
|
if "{subject}" in positive:
|
||||||
|
positive = positive.replace("{subject}", subject)
|
||||||
|
elif not positive:
|
||||||
|
positive = subject
|
||||||
|
else:
|
||||||
|
positive = f"{subject}, {positive}"
|
||||||
|
triggers = scaffold.get("trigger_words") or []
|
||||||
|
if triggers:
|
||||||
|
positive = ", ".join(list(triggers) + [positive]) if positive else ", ".join(triggers)
|
||||||
|
return positive
|
||||||
|
|
||||||
|
|
||||||
|
def comfyui_generate_with_skill_oneshot(
|
||||||
|
slug: str,
|
||||||
|
subject: str,
|
||||||
|
*,
|
||||||
|
server: str = "127.0.0.1:8188",
|
||||||
|
dest: str | None = None,
|
||||||
|
seed: int = 0,
|
||||||
|
judge: bool = True,
|
||||||
|
recipe_patch: dict | None = None,
|
||||||
|
library_dir: str | None = None,
|
||||||
|
wait_timeout: float = 600.0,
|
||||||
|
) -> dict:
|
||||||
|
"""Genera (y opcionalmente juzga) una imagen a partir de una skill, end-to-end.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
slug: slug de la skill a usar (su carpeta en la librería).
|
||||||
|
subject: sujeto concreto que sustituye ``{subject}`` en el scaffold del prompt
|
||||||
|
(p.ej. "a woman with red hair").
|
||||||
|
server: host:port del servidor ComfyUI (sin esquema). keyword-only.
|
||||||
|
dest: directorio local donde guardar el PNG (None = cwd). keyword-only.
|
||||||
|
seed: semilla de generación. keyword-only.
|
||||||
|
judge: si True, puntúa el PNG con el panel `comfyui-judge` y acumula el score en la
|
||||||
|
skill (salvo que se pase ``recipe_patch``). keyword-only.
|
||||||
|
recipe_patch: dict de cambios aplicados a la receta EN MEMORIA antes de compilar
|
||||||
|
(deep-merge), para probar una variante sin guardarla. Con patch, el score NO se
|
||||||
|
acumula en ``score_mean``. keyword-only.
|
||||||
|
library_dir: raíz de la librería. Default ``~/ComfyUI/skills_library``. keyword-only.
|
||||||
|
wait_timeout: segundos máximos esperando al servidor. keyword-only.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict ``{ok, slug, prompt_id, image_path, prompt_resolved, judge, judge_run_id,
|
||||||
|
score_mean, score_n, error}``. ``judge`` = ``{verdict, score, votes}`` (o None si
|
||||||
|
``judge=False`` o el panel falla). ``judge_run_id`` correlaciona el juicio (sirve de
|
||||||
|
evidencia para `comfyui_bump_skill_version`). Si falla, ``ok=False`` y ``error``
|
||||||
|
explica en qué paso.
|
||||||
|
"""
|
||||||
|
base = {"ok": False, "slug": slug, "prompt_id": "", "image_path": "",
|
||||||
|
"prompt_resolved": "", "judge": None, "judge_run_id": "",
|
||||||
|
"score_mean": None, "score_n": None, "error": ""}
|
||||||
|
|
||||||
|
# 1. Cargar la receta de la skill.
|
||||||
|
loaded = comfyui_load_skill(slug, library_dir=library_dir)
|
||||||
|
if not loaded.get("ok"):
|
||||||
|
return {**base, "error": f"load_skill falló: {loaded.get('error')}"}
|
||||||
|
recipe = loaded["recipe"]
|
||||||
|
if recipe_patch:
|
||||||
|
recipe = _deep_merge(recipe, recipe_patch)
|
||||||
|
|
||||||
|
prompt_resolved = _resolve_prompt(recipe, subject)
|
||||||
|
|
||||||
|
# 2. Compilar la receta a un workflow (función pura del registry).
|
||||||
|
try:
|
||||||
|
workflow = build_skill_workflow(recipe, subject, seed=seed)
|
||||||
|
except SkillWorkflowError as exc:
|
||||||
|
return {**base, "prompt_resolved": prompt_resolved,
|
||||||
|
"error": f"build_skill_workflow falló: {exc}"}
|
||||||
|
|
||||||
|
# 3. Encolar.
|
||||||
|
try:
|
||||||
|
sub = comfyui_submit_workflow(workflow, server=server)
|
||||||
|
prompt_id = sub["prompt_id"]
|
||||||
|
except (RuntimeError, KeyError) as exc:
|
||||||
|
return {**base, "prompt_resolved": prompt_resolved,
|
||||||
|
"error": f"submit falló: {exc}"}
|
||||||
|
|
||||||
|
# 4. Esperar a que termine.
|
||||||
|
try:
|
||||||
|
outputs = comfyui_wait_result(prompt_id, server=server, timeout=wait_timeout)
|
||||||
|
except (TimeoutError, RuntimeError) as exc:
|
||||||
|
return {**base, "prompt_id": prompt_id, "prompt_resolved": prompt_resolved,
|
||||||
|
"error": f"wait falló: {exc}"}
|
||||||
|
|
||||||
|
# 5. Localizar el primer PNG en los outputs (nodo SaveImage → images).
|
||||||
|
img = None
|
||||||
|
for node_out in outputs.values():
|
||||||
|
images = node_out.get("images") if isinstance(node_out, dict) else None
|
||||||
|
if images:
|
||||||
|
img = images[0]
|
||||||
|
break
|
||||||
|
if img is None:
|
||||||
|
return {**base, "prompt_id": prompt_id, "prompt_resolved": prompt_resolved,
|
||||||
|
"error": f"el workflow no produjo imágenes (outputs={list(outputs)})"}
|
||||||
|
|
||||||
|
# 6. Descargar la imagen a disco.
|
||||||
|
fetched = comfyui_fetch_output_image(
|
||||||
|
img["filename"], subfolder=img.get("subfolder", ""),
|
||||||
|
type_=img.get("type", "output"), server=server, dest_dir=dest or ".",
|
||||||
|
)
|
||||||
|
if not fetched.get("ok"):
|
||||||
|
return {**base, "prompt_id": prompt_id, "prompt_resolved": prompt_resolved,
|
||||||
|
"error": f"fetch de imagen falló: {fetched.get('error')}"}
|
||||||
|
image_path = fetched["path"]
|
||||||
|
|
||||||
|
result = {**base, "ok": True, "prompt_id": prompt_id, "image_path": image_path,
|
||||||
|
"prompt_resolved": prompt_resolved}
|
||||||
|
|
||||||
|
if not judge:
|
||||||
|
return result
|
||||||
|
|
||||||
|
# 7. Juzgar el resultado con el panel multi-juez.
|
||||||
|
judge_run_id = f"judge_{prompt_id}"
|
||||||
|
verdict = comfyui_judge_image(image_path, prompt_resolved, server=server)
|
||||||
|
if not verdict.get("ok"):
|
||||||
|
# El panel falló entero (los 3 jueces caídos): la imagen ya está, no es error fatal.
|
||||||
|
result["judge_run_id"] = judge_run_id
|
||||||
|
result["error"] = f"juez falló (imagen generada igualmente): {verdict.get('error')}"
|
||||||
|
return result
|
||||||
|
|
||||||
|
result["judge"] = {"verdict": verdict["verdict"], "score": verdict["score"],
|
||||||
|
"votes": verdict["votes"]}
|
||||||
|
result["judge_run_id"] = judge_run_id
|
||||||
|
|
||||||
|
# 8. Acumular el score en la media de la skill — solo para la receta canónica
|
||||||
|
# (una variante con recipe_patch es una prueba; el caller decide si la promueve).
|
||||||
|
if not recipe_patch:
|
||||||
|
upd = comfyui_update_skill_score(slug, verdict["score"], library_dir=library_dir)
|
||||||
|
if upd.get("ok"):
|
||||||
|
result["score_mean"] = upd["score_mean"]
|
||||||
|
result["score_n"] = upd["score_n"]
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# Alias con el nombre completo del ID para descubrimiento por convención.
|
||||||
|
generate_with_skill_oneshot = comfyui_generate_with_skill_oneshot
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import json
|
||||||
|
|
||||||
|
res = comfyui_generate_with_skill_oneshot(
|
||||||
|
"portrait_cinematic_sd15", "a woman with red hair",
|
||||||
|
dest="/tmp/comfy_skill", seed=42, judge=True,
|
||||||
|
)
|
||||||
|
print(json.dumps(res, indent=2, ensure_ascii=False))
|
||||||
Reference in New Issue
Block a user