Files
FuryNocturnTV a6bd615029 fix(2.0.5): security patch, performance & ComfyUI 2.0 compatibility
fix: EmbeddingsSelector scroll not working in legacy canvas renderer
fix: EmbeddingsSelector checkboxes missing in ComfyUI 2.0 (addDOMWidget)
fix: ActionAnimator VRAM exhaustion — return 1 frame + frame_count metadata
fix: NODE_CLASS_MAPPINGS unreachable inside return block in 7 node files
fix: sf_tools.js infinite retry loop on ComfyUI 2.0 menu API

security: torch.load() replaced with weights_only=True in FuryFileManager
security: fury asset schema validation before returning loaded data

perf: gc.collect() and empty_cache() moved outside render loop in DirectorEngine
perf: ProgressBar added to DirectorEngine batch render
perf: PNG preview compress_level reduced from 4 to 1

refactor: get_lang_text() centralized in utils/sf_lang.py
refactor: fury_bus.copy() replaced with deepcopy() in SF_AddEntity
refactor: os.walk() now skips non-Python dirs (.idea, interface, js, node_modules)
refactor: diagnostic system added to __init__.py with /studiofury/diagnostics API

chore: VERSION synced to 2.0.5 (was 1.2.6)
chore: removed unused `import sys` in SmartSaver and ProjectManager
chore: SF_AssetLoader dual-key fallback explicitly documented

docs: README updated (es/en)
docs: Wiki updated (es/en)
2026-05-06 00:48:02 +02:00

493 lines
19 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { app } from "/scripts/app.js";
// ============================================================
// StudioFury — EmbeddingsSelector (Legacy + ComfyUI 2.0)
// Detecta la versión del frontend y usa el sistema correcto.
// ============================================================
const IS_COMFY_V2 = !!window.__COMFYUI_FRONTEND_VERSION__
|| !!document.querySelector?.("#comfy-vue-app, #comfy-app")
|| typeof globalThis.useToastStore !== "undefined";
// ── Parámetros visuales compartidos ─────────────────────────
const ROW_HEIGHT = 24;
const MAX_VISIBLE = 10;
const HEADER_HEIGHT = 70;
const SCROLLBAR_W = 16;
const NODE_WIDTH = 370;
// ============================================================
// HELPERS comunes
// ============================================================
function buildOutputText(selection) {
return Object.entries(selection)
.filter(([, v]) => v)
.map(([k, v]) => v + ":" + k)
.join("|");
}
// ============================================================
// RUTA A — Legacy canvas renderer (pre-2.0)
// ============================================================
function registerLegacy(nodeType) {
const _onCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const r = _onCreated?.apply(this, arguments);
this._sf_list = [];
this._sf_sel = {};
this._sf_scroll = 0;
const fixedH = HEADER_HEIGHT + MAX_VISIBLE * ROW_HEIGHT + 20;
this.setSize([NODE_WIDTH, fixedH]);
// Ocultar widgets nativos y capturar la lista de embeddings
if (this.widgets) {
for (const w of this.widgets) {
if (w.name === "selected_data") { w.hidden = true; }
if (w.name === "embedding_list_raw") {
// La lista viene como w.options.values o w.type (combo)
const vals = w.options?.values ?? (Array.isArray(w.value) ? w.value : null);
if (vals) this._sf_list = [...vals];
w.hidden = true;
}
}
}
// ── Scroll con rueda ─────────────────────────────────
// LiteGraph llama onMouseWheel SOLO si el nodo lo implementa
// y el canvas ya no consume el evento primero.
// Para garantizarlo añadimos también un listener DOM directo.
this._sf_wheelHandler = (e) => {
if (!this._sf_hovered) return;
const maxOff = Math.max(0, this._sf_list.length - MAX_VISIBLE);
this._sf_scroll = Math.max(0, Math.min(maxOff,
this._sf_scroll + (e.deltaY > 0 ? 1 : -1)));
this.setDirtyCanvas(true, true);
e.preventDefault();
e.stopPropagation();
};
// Buscamos el canvas del editor para adjuntar el handler
const canvas = document.getElementById("graph-canvas")
?? document.querySelector("canvas.litegraph");
if (canvas) {
canvas.addEventListener("wheel", this._sf_wheelHandler, { passive: false });
}
// ── Hover tracking ───────────────────────────────────
this.onMouseEnter = () => { this._sf_hovered = true; };
this.onMouseLeave = () => { this._sf_hovered = false; };
return r;
};
// Limpieza al eliminar el nodo
const _onRemoved = nodeType.prototype.onRemoved;
nodeType.prototype.onRemoved = function () {
if (this._sf_wheelHandler) {
const canvas = document.getElementById("graph-canvas")
?? document.querySelector("canvas.litegraph");
canvas?.removeEventListener("wheel", this._sf_wheelHandler);
}
_onRemoved?.apply(this, arguments);
};
// ── Bloqueo de tamaño ────────────────────────────────────
nodeType.prototype.onResize = function (size) {
size[0] = NODE_WIDTH;
size[1] = HEADER_HEIGHT + MAX_VISIBLE * ROW_HEIGHT + 20;
};
// ── Dibujado ─────────────────────────────────────────────
nodeType.prototype.onDrawForeground = function (ctx) {
if (this.flags?.collapsed) return;
ctx.save();
ctx.font = "12px Arial, sans-serif";
// Cabecera
const headerY = 50;
ctx.fillStyle = "#aaa";
ctx.fillText("Embedding", 10, headerY);
ctx.fillText("POS", 265, headerY);
ctx.fillText("NEG", 310, headerY);
ctx.strokeStyle = "#444";
ctx.beginPath();
ctx.moveTo(10, headerY + 8);
ctx.lineTo(this.size[0] - 20, headerY + 8);
ctx.stroke();
const startY = HEADER_HEIGHT;
const list = this._sf_list ?? [];
const sel = this._sf_sel ?? {};
const scroll = this._sf_scroll ?? 0;
if (list.length === 0) {
ctx.fillStyle = "#666";
ctx.fillText("No embeddings found", 10, startY + 16);
ctx.restore();
return;
}
const visible = list.slice(scroll, scroll + MAX_VISIBLE);
for (let i = 0; i < visible.length; i++) {
const name = visible[i];
const rowY = startY + i * ROW_HEIGHT + ROW_HEIGHT / 2;
// Fila alternada
if (i % 2 === 0) {
ctx.fillStyle = "rgba(255,255,255,0.03)";
ctx.fillRect(6, startY + i * ROW_HEIGHT, this.size[0] - 12, ROW_HEIGHT);
}
// Nombre (truncado)
ctx.fillStyle = "#ccc";
let label = name.replace(/\.(pt|bin|safetensors)$/i, "");
if (label.length > 30) label = label.slice(0, 27) + "…";
ctx.fillText(label, 10, rowY + 1);
// Checkbox POS
const isPos = sel[name] === "P";
ctx.fillStyle = isPos ? "#22c55e" : "#1e1e1e";
ctx.beginPath();
ctx.roundRect(260, rowY - 8, 16, 16, 3);
ctx.fill();
ctx.strokeStyle = isPos ? "#16a34a" : "#555";
ctx.lineWidth = 1;
ctx.stroke();
if (isPos) {
ctx.strokeStyle = "#fff";
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(263, rowY); ctx.lineTo(266, rowY + 4); ctx.lineTo(273, rowY - 4);
ctx.stroke();
}
// Checkbox NEG
const isNeg = sel[name] === "N";
ctx.fillStyle = isNeg ? "#ef4444" : "#1e1e1e";
ctx.beginPath();
ctx.roundRect(310, rowY - 8, 16, 16, 3);
ctx.fill();
ctx.strokeStyle = isNeg ? "#dc2626" : "#555";
ctx.lineWidth = 1;
ctx.stroke();
if (isNeg) {
ctx.strokeStyle = "#fff";
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(313, rowY); ctx.lineTo(316, rowY + 4); ctx.lineTo(323, rowY - 4);
ctx.stroke();
}
}
// ── Scrollbar ─────────────────────────────────────────
if (list.length > MAX_VISIBLE) {
const trackH = MAX_VISIBLE * ROW_HEIGHT;
const sbX = this.size[0] - SCROLLBAR_W - 4;
const maxOff = list.length - MAX_VISIBLE;
// Track
ctx.fillStyle = "#111";
ctx.beginPath();
ctx.roundRect(sbX, startY, SCROLLBAR_W, trackH, 4);
ctx.fill();
// Thumb
const ratio = MAX_VISIBLE / list.length;
const thumbH = Math.max(20, trackH * ratio);
const thumbY = startY + (scroll / maxOff) * (trackH - thumbH);
ctx.fillStyle = "#555";
ctx.beginPath();
ctx.roundRect(sbX + 2, thumbY + 2, SCROLLBAR_W - 4, thumbH - 4, 4);
ctx.fill();
// Flechas
const arrowX = sbX + SCROLLBAR_W / 2;
ctx.fillStyle = "#888";
// ▲ arriba
ctx.beginPath();
ctx.moveTo(arrowX, startY + 4);
ctx.lineTo(arrowX - 5, startY + 12);
ctx.lineTo(arrowX + 5, startY + 12);
ctx.closePath(); ctx.fill();
// ▼ abajo
const botY = startY + trackH;
ctx.beginPath();
ctx.moveTo(arrowX, botY - 4);
ctx.lineTo(arrowX - 5, botY - 12);
ctx.lineTo(arrowX + 5, botY - 12);
ctx.closePath(); ctx.fill();
}
// Contador inferior
ctx.fillStyle = "#555";
ctx.font = "10px Arial, sans-serif";
const total = list.length;
const showing = Math.min(MAX_VISIBLE, total - scroll);
ctx.fillText(`${scroll + 1}–${scroll + showing} of ${total}`, 10,
startY + MAX_VISIBLE * ROW_HEIGHT + 14);
ctx.restore();
};
// ── Clics ────────────────────────────────────────────────
nodeType.prototype.onMouseDown = function (e, pos) {
const [mx, my] = pos;
const list = this._sf_list ?? [];
const sel = this._sf_sel ?? {};
const scroll = this._sf_scroll ?? 0;
const startY = HEADER_HEIGHT - ROW_HEIGHT * 0.5;
const listH = MAX_VISIBLE * ROW_HEIGHT;
const sbX = this.size[0] - SCROLLBAR_W - 4;
const maxOff = Math.max(0, list.length - MAX_VISIBLE);
// ── Flechas de scroll ──
if (mx > sbX) {
const trackBot = HEADER_HEIGHT + MAX_VISIBLE * ROW_HEIGHT;
if (my > HEADER_HEIGHT && my < HEADER_HEIGHT + 14) {
this._sf_scroll = Math.max(0, scroll - 1);
this.setDirtyCanvas(true, true); return true;
}
if (my > trackBot - 14 && my < trackBot + 4) {
this._sf_scroll = Math.min(maxOff, scroll + 1);
this.setDirtyCanvas(true, true); return true;
}
// Clic en la track → saltar a posición proporcional
const trackH = MAX_VISIBLE * ROW_HEIGHT;
const rel = (my - HEADER_HEIGHT) / trackH;
this._sf_scroll = Math.round(rel * maxOff);
this._sf_scroll = Math.max(0, Math.min(maxOff, this._sf_scroll));
this.setDirtyCanvas(true, true); return true;
}
// ── Checkboxes ──
if (my > startY && my < startY + listH) {
const rowIdx = Math.floor((my - startY) / ROW_HEIGHT);
const realIdx = scroll + rowIdx;
if (realIdx >= 0 && realIdx < list.length) {
const name = list[realIdx];
if (mx >= 260 && mx <= 280) {
this._sf_sel[name] = sel[name] === "P" ? null : "P";
this._updateOutput();
this.setDirtyCanvas(true, true); return true;
}
if (mx >= 310 && mx <= 330) {
this._sf_sel[name] = sel[name] === "N" ? null : "N";
this._updateOutput();
this.setDirtyCanvas(true, true); return true;
}
}
}
};
nodeType.prototype._updateOutput = function () {
const w = this.widgets?.find(w => w.name === "selected_data");
if (w) w.value = buildOutputText(this._sf_sel);
};
// onMouseWheel de LiteGraph (fallback, por si el DOM listener no funciona)
nodeType.prototype.onMouseWheel = function (e) {
const maxOff = Math.max(0, (this._sf_list?.length ?? 0) - MAX_VISIBLE);
this._sf_scroll = Math.max(0, Math.min(maxOff,
(this._sf_scroll ?? 0) + (e.deltaY > 0 ? 1 : -1)));
this.setDirtyCanvas(true, true);
return true; // consume el evento
};
}
// ============================================================
// RUTA B — ComfyUI 2.0 (DOM widget + addDOMWidget)
// ============================================================
function registerV2(nodeType) {
const _onCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const r = _onCreated?.apply(this, arguments);
// Extraemos la lista de embeddings del widget nativo antes de ocultarlo
let embList = [];
if (this.widgets) {
for (const w of this.widgets) {
if (w.name === "embedding_list_raw") {
const vals = w.options?.values ?? [];
embList = [...vals];
w.hidden = true;
}
if (w.name === "selected_data") { w.hidden = true; }
}
}
// ── Construir el widget DOM ──────────────────────────
const container = document.createElement("div");
container.style.cssText = `
width: 100%;
max-height: ${MAX_VISIBLE * 28 + 40}px;
overflow-y: auto;
font-size: 12px;
font-family: Arial, sans-serif;
box-sizing: border-box;
padding: 4px 0;
`;
// Cabecera
const header = document.createElement("div");
header.style.cssText = `
display: grid;
grid-template-columns: 1fr 44px 44px;
padding: 4px 8px;
border-bottom: 1px solid rgba(255,255,255,0.1);
font-weight: 600;
color: #999;
position: sticky;
top: 0;
background: rgba(30,30,30,0.95);
z-index: 1;
`;
header.innerHTML = `
<span>Embedding</span>
<span style="text-align:center;color:#22c55e">POS</span>
<span style="text-align:center;color:#ef4444">NEG</span>
`;
container.appendChild(header);
// Lista de filas
const selection = {};
const node = this;
function updateOutput() {
const w = node.widgets?.find(w => w.name === "selected_data");
if (w) w.value = buildOutputText(selection);
}
function makeRow(name, idx) {
const row = document.createElement("div");
row.style.cssText = `
display: grid;
grid-template-columns: 1fr 44px 44px;
align-items: center;
padding: 3px 8px;
border-radius: 4px;
background: ${idx % 2 === 0 ? "rgba(255,255,255,0.02)" : "transparent"};
transition: background 0.1s;
`;
row.addEventListener("mouseenter", () => {
row.style.background = "rgba(255,255,255,0.06)";
});
row.addEventListener("mouseleave", () => {
row.style.background = idx % 2 === 0
? "rgba(255,255,255,0.02)" : "transparent";
});
// Nombre
const label = document.createElement("span");
let display = name.replace(/\.(pt|bin|safetensors)$/i, "");
if (display.length > 30) display = display.slice(0, 27) + "…";
label.title = name;
label.textContent = display;
label.style.cssText = "color:#ccc; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;";
// Botón POS
const btnPos = document.createElement("button");
btnPos.textContent = "P";
styleBtn(btnPos, false, "pos");
btnPos.addEventListener("click", (e) => {
e.stopPropagation();
selection[name] = selection[name] === "P" ? null : "P";
styleBtn(btnPos, selection[name] === "P", "pos");
styleBtn(btnNeg, false, "neg");
updateOutput();
});
// Botón NEG
const btnNeg = document.createElement("button");
btnNeg.textContent = "N";
styleBtn(btnNeg, false, "neg");
btnNeg.addEventListener("click", (e) => {
e.stopPropagation();
selection[name] = selection[name] === "N" ? null : "N";
styleBtn(btnNeg, selection[name] === "N", "neg");
styleBtn(btnPos, false, "pos");
updateOutput();
});
row.appendChild(label);
row.appendChild(btnPos);
row.appendChild(btnNeg);
return { row, btnPos, btnNeg };
}
if (embList.length === 0) {
const empty = document.createElement("div");
empty.style.cssText = "color:#555; padding:8px; font-size:11px;";
empty.textContent = "No embeddings found";
container.appendChild(empty);
} else {
embList.forEach((name, i) => {
const { row } = makeRow(name, i);
container.appendChild(row);
});
}
// Prevenir que el scroll del widget llegue al canvas
container.addEventListener("wheel", (e) => { e.stopPropagation(); }, { passive: true });
// Registrar como DOM widget
this.addDOMWidget("sf_embeddings_ui", "div", container, {
getValue: () => buildOutputText(selection),
setValue: (v) => {
// Si se restaura un estado guardado, sincronizar
if (!v) return;
v.split("|").forEach(item => {
if (item.startsWith("P:")) selection[item.slice(2)] = "P";
else if (item.startsWith("N:")) selection[item.slice(2)] = "N";
});
}
});
return r;
};
}
// ── Estilo de botones para modo V2 ───────────────────────────
function styleBtn(btn, active, type) {
const activeColor = type === "pos" ? "#22c55e" : "#ef4444";
const activeBorder = type === "pos" ? "#16a34a" : "#dc2626";
btn.style.cssText = `
width: 28px; height: 22px;
border-radius: 4px;
border: 1px solid ${active ? activeBorder : "#444"};
background: ${active ? activeColor : "#1e1e1e"};
color: ${active ? "#fff" : "#666"};
font-size: 11px; font-weight: 600;
cursor: pointer;
margin: 0 auto;
display: block;
transition: background 0.15s, border-color 0.15s;
`;
}
// ============================================================
// REGISTRO DE LA EXTENSIÓN
// ============================================================
app.registerExtension({
name: "StudioFury.EmbeddingsSelector",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData.name !== "StudioFury_EmbeddingsSelector") return;
console.log(`🧩 [StudioFury] EmbeddingsSelector → modo: ${IS_COMFY_V2 ? "ComfyUI 2.0" : "Legacy"}`);
if (IS_COMFY_V2) {
registerV2(nodeType);
} else {
registerLegacy(nodeType);
}
}
});