Phase 6: train a second LoRA for real Penpot UI design capability #5
@@ -99,6 +99,10 @@ AUDIT_MAX_RETRIES = int(os.environ.get("GATE5_AUDIT_RETRIES", "5"))
|
|||||||
# inspeccion manual con GATE5_KEEP_PAGES=1.
|
# inspeccion manual con GATE5_KEEP_PAGES=1.
|
||||||
LIMPIAR_PAGINAS = os.environ.get("GATE5_KEEP_PAGES", "").lower() not in ("1", "true", "yes")
|
LIMPIAR_PAGINAS = os.environ.get("GATE5_KEEP_PAGES", "").lower() not in ("1", "true", "yes")
|
||||||
|
|
||||||
|
# Tamano de lote. Ver el comentario del bucle en run_full(): el plugin se degrada tras 5 o 6
|
||||||
|
# prompts pesados, asi que 4 deja margen y da un punto de re-handshake antes del deterioro.
|
||||||
|
BATCH_SIZE = int(os.environ.get("GATE5_BATCH_SIZE", "4"))
|
||||||
|
|
||||||
# El prompt que reproduce el fallo exacto de produccion. Tiene su propia condicion de
|
# El prompt que reproduce el fallo exacto de produccion. Tiene su propia condicion de
|
||||||
# aprobacion: si este no llega a 60, la puerta no aprueba aunque el promedio alcance.
|
# aprobacion: si este no llega a 60, la puerta no aprueba aunque el promedio alcance.
|
||||||
FLAGSHIP_PROMPT_ID = "g5-06-landing-pizzeria"
|
FLAGSHIP_PROMPT_ID = "g5-06-landing-pizzeria"
|
||||||
@@ -635,17 +639,28 @@ if (principal) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let cssLen = 0, markupLen = 0, renderError = null;
|
// generateStyle con includeChildren y generateMarkup son, de lejos, lo mas caro de la auditoria:
|
||||||
try {
|
// serializan el subarbol entero. El servidor MCP corta cualquier tarea del plugin a los 30
|
||||||
const objetivo = principal ? [principal] : top;
|
// segundos -- ese limite es SUYO, no del cliente, asi que no se puede subir desde aca; lo unico
|
||||||
if (objetivo.length > 0) {
|
// que se puede hacer es no acercarse. Por encima de MAX_NODOS_RENDER se saltea y se deja
|
||||||
const css = penpot.generateStyle(objetivo, { type: "css", includeChildren: true }) || "";
|
// constancia, en vez de arriesgar que se caiga la auditoria ENTERA y con ella la medicion del
|
||||||
const markup = penpot.generateMarkup(objetivo, { type: "html" }) || "";
|
// prompt. `rendersOk` queda en null (no aplica) y no entra al denominador del score.
|
||||||
cssLen = String(css).length;
|
const MAX_NODOS_RENDER = 400;
|
||||||
markupLen = String(markup).length;
|
let cssLen = 0, markupLen = 0, renderError = null, renderSalteado = false;
|
||||||
|
if (nodes.length > MAX_NODOS_RENDER) {
|
||||||
|
renderSalteado = true;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const objetivo = principal ? [principal] : top;
|
||||||
|
if (objetivo.length > 0) {
|
||||||
|
const css = penpot.generateStyle(objetivo, { type: "css", includeChildren: true }) || "";
|
||||||
|
const markup = penpot.generateMarkup(objetivo, { type: "html" }) || "";
|
||||||
|
cssLen = String(css).length;
|
||||||
|
markupLen = String(markup).length;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
renderError = String((e && e.message) || e);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
renderError = String((e && e.message) || e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const tamsUnicos = [];
|
const tamsUnicos = [];
|
||||||
@@ -749,7 +764,8 @@ const resultado = {
|
|||||||
rootHeightMismatch: desajusteAltura,
|
rootHeightMismatch: desajusteAltura,
|
||||||
cssLength: cssLen,
|
cssLength: cssLen,
|
||||||
markupLength: markupLen,
|
markupLength: markupLen,
|
||||||
renderError: renderError
|
renderError: renderError,
|
||||||
|
renderSkipped: renderSalteado
|
||||||
};
|
};
|
||||||
|
|
||||||
// El servidor MCP valida el resultado contra un schema y rechaza la llamada ENTERA si algun
|
// El servidor MCP valida el resultado contra un schema y rechaza la llamada ENTERA si algun
|
||||||
@@ -1062,8 +1078,13 @@ def build_raw_metrics(audit, runtime):
|
|||||||
"paletteStructured": bool(audit.get("paletteStructured", False)),
|
"paletteStructured": bool(audit.get("paletteStructured", False)),
|
||||||
"finalMessageListsHex": runtime["finalMessageListsHex"],
|
"finalMessageListsHex": runtime["finalMessageListsHex"],
|
||||||
"fontIdBoundShare": audit.get("fontIdBoundShare", 0.0),
|
"fontIdBoundShare": audit.get("fontIdBoundShare", 0.0),
|
||||||
"rendersOk": bool(audit.get("cssLength", 0) > 0 and audit.get("markupLength", 0) > 0
|
# Si la auditoria salteo el render por tamano, la metrica NO APLICA (None): no se puede
|
||||||
and not audit.get("renderError")),
|
# afirmar que el arbol renderiza ni que no renderiza, y contarla como fallo castigaria
|
||||||
|
# justo a los disenos grandes, que es lo contrario de lo que la puerta quiere premiar.
|
||||||
|
"rendersOk": (None if audit.get("renderSkipped")
|
||||||
|
else bool(audit.get("cssLength", 0) > 0
|
||||||
|
and audit.get("markupLength", 0) > 0
|
||||||
|
and not audit.get("renderError"))),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1088,7 +1109,11 @@ def score_prompt(prompt_id, raw):
|
|||||||
veto = []
|
veto = []
|
||||||
for clave, etiqueta, comparador in METRICS:
|
for clave, etiqueta, comparador in METRICS:
|
||||||
umbral = umbrales.get(clave)
|
umbral = umbrales.get(clave)
|
||||||
ok = evaluar(clave, comparador, raw[clave], umbral)
|
# Una metrica NO APLICA si su umbral es None (el prompt no la exige) o si su VALOR es
|
||||||
|
# None (la auditoria no la pudo computar, p.ej. el render salteado por tamano). Contar
|
||||||
|
# "no se pudo medir" como fallo castigaria al diseno grande, que es lo contrario de lo
|
||||||
|
# que la puerta premia.
|
||||||
|
ok = None if raw[clave] is None else evaluar(clave, comparador, raw[clave], umbral)
|
||||||
detalle.append({
|
detalle.append({
|
||||||
"metrica": clave, "etiqueta": etiqueta, "valor": raw[clave],
|
"metrica": clave, "etiqueta": etiqueta, "valor": raw[clave],
|
||||||
"umbral": umbral, "aplica": ok is not None, "pasa": ok,
|
"umbral": umbral, "aplica": ok is not None, "pasa": ok,
|
||||||
@@ -1689,18 +1714,54 @@ def run_full(cfg, tools, system_prompt):
|
|||||||
|
|
||||||
t0 = time.time()
|
t0 = time.time()
|
||||||
resultados = []
|
resultados = []
|
||||||
for fila in prompts:
|
pendientes = []
|
||||||
try:
|
corte_por_degradacion = False
|
||||||
resultados.append(run_prompt(cfg, mcp, tools, system_prompt, fila, png_dir))
|
|
||||||
except PluginNotConnected:
|
# LOTES. El plugin de Penpot aguanta de forma confiable unos 5 o 6 prompts pesados y despues
|
||||||
|
# se degrada hasta dar timeouts de 30 s en `createPage`. Medido en tres corridas seguidas,
|
||||||
|
# siempre con la misma forma: los primeros funcionan, el resto falla en el setup sin
|
||||||
|
# excepcion. Procesar en lotes chicos con un handshake limpio en el medio le da al servidor
|
||||||
|
# un punto de recuperacion, y si igual se degrada se corta ahi en vez de quemar los que
|
||||||
|
# faltan de a uno.
|
||||||
|
for inicio in range(0, len(prompts), BATCH_SIZE):
|
||||||
|
lote = prompts[inicio:inicio + BATCH_SIZE]
|
||||||
|
n_lote = inicio // BATCH_SIZE + 1
|
||||||
|
total_lotes = (len(prompts) + BATCH_SIZE - 1) // BATCH_SIZE
|
||||||
|
if inicio > 0:
|
||||||
|
print(f"\n[LOTE] handshake limpio antes del lote {n_lote}/{total_lotes}")
|
||||||
|
mcp.session_id = None
|
||||||
|
try:
|
||||||
|
mcp.handshake()
|
||||||
|
except Gate5Error as e:
|
||||||
|
print(f"[LOTE] el re-handshake fallo ({e}); se corta y quedan pendientes "
|
||||||
|
f"{len(prompts) - inicio} prompt(s)")
|
||||||
|
pendientes.extend(f["id"] for f in prompts[inicio:])
|
||||||
|
corte_por_degradacion = True
|
||||||
|
break
|
||||||
|
print(f"\n[LOTE] {n_lote}/{total_lotes}: {[f['id'] for f in lote]}")
|
||||||
|
|
||||||
|
for i, fila in enumerate(lote):
|
||||||
|
try:
|
||||||
|
resultados.append(run_prompt(cfg, mcp, tools, system_prompt, fila, png_dir))
|
||||||
|
except PluginNotConnected:
|
||||||
|
volcar(resultados, time.time() - t0)
|
||||||
|
raise
|
||||||
|
except Gate5Error as e:
|
||||||
|
# Un fallo de setup NO quema el prompt: queda PENDIENTE, no medido con score
|
||||||
|
# None. Y se corta el lote, porque una vez que el plugin empieza a dar timeouts
|
||||||
|
# los que siguen fallan todos igual -- insistir solo gasta paginas y ensucia el
|
||||||
|
# JSON.
|
||||||
|
print(f"[ERROR] {fila['id']}: {e}")
|
||||||
|
restantes = [f["id"] for f in lote[i:]] + \
|
||||||
|
[f["id"] for f in prompts[inicio + BATCH_SIZE:]]
|
||||||
|
print(f"[LOTE] se corta el lote. Quedan {len(restantes)} prompt(s) PENDIENTES "
|
||||||
|
f"(no medidos, no quemados): {restantes}")
|
||||||
|
pendientes.extend(restantes)
|
||||||
|
corte_por_degradacion = True
|
||||||
|
break
|
||||||
volcar(resultados, time.time() - t0)
|
volcar(resultados, time.time() - t0)
|
||||||
raise
|
if corte_por_degradacion:
|
||||||
except Gate5Error as e:
|
break
|
||||||
print(f"[ERROR] {fila['id']}: {e}")
|
|
||||||
print(f"[INFO] se guardan los {len(resultados)} prompt(s) ya medidos y se sigue con "
|
|
||||||
f"el siguiente; el JSON queda marcado como parcial")
|
|
||||||
resultados.append({"id": fila["id"], "error": str(e), "score": None})
|
|
||||||
volcar(resultados, time.time() - t0)
|
|
||||||
dt = time.time() - t0
|
dt = time.time() - t0
|
||||||
|
|
||||||
aprueba, veredicto = resumen(fusionar(resultados), cfg["GATE5_TAG"],
|
aprueba, veredicto = resumen(fusionar(resultados), cfg["GATE5_TAG"],
|
||||||
@@ -1708,7 +1769,8 @@ def run_full(cfg, tools, system_prompt):
|
|||||||
print(f"\n tiempo total: {dt / 60:.1f} min")
|
print(f"\n tiempo total: {dt / 60:.1f} min")
|
||||||
|
|
||||||
volcar(resultados, dt, veredicto_parcial=veredicto, completo=True)
|
volcar(resultados, dt, veredicto_parcial=veredicto, completo=True)
|
||||||
sin_medir = [r["id"] for r in fusionar(resultados) if r.get("score") is None]
|
sin_medir = sorted(set(pendientes) |
|
||||||
|
{r["id"] for r in fusionar(resultados) if r.get("score") is None})
|
||||||
if sin_medir:
|
if sin_medir:
|
||||||
print(f"\n[PLUGIN DEGRADADO] {len(sin_medir)} prompt(s) no se pudieron medir. El plugin "
|
print(f"\n[PLUGIN DEGRADADO] {len(sin_medir)} prompt(s) no se pudieron medir. El plugin "
|
||||||
f"de Penpot responde pero se degrada tras 5 o 6 prompts pesados.")
|
f"de Penpot responde pero se degrada tras 5 o 6 prompts pesados.")
|
||||||
@@ -1716,8 +1778,8 @@ def run_full(cfg, tools, system_prompt):
|
|||||||
print(f" GATE5_ONLY={','.join(sin_medir)}")
|
print(f" GATE5_ONLY={','.join(sin_medir)}")
|
||||||
return EXIT_PLUGIN_DEGRADADO
|
return EXIT_PLUGIN_DEGRADADO
|
||||||
print(f" resultados en {salida.relative_to(REPO_ROOT)}")
|
print(f" resultados en {salida.relative_to(REPO_ROOT)}")
|
||||||
print(f" PNGs en {png_dir.relative_to(REPO_ROOT)}/ (la puerta no borra nada: las paginas "
|
print(f" PNGs en {png_dir.relative_to(REPO_ROOT)}/ (la pagina de cada prompt se vacia tras "
|
||||||
f"quedan para inspeccion humana)")
|
f"exportar su PNG; solo se conserva la de un prompt cuyo PNG haya fallado)")
|
||||||
return 0 if aprueba else 1
|
return 0 if aprueba else 1
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user