feat: inject start_name/middle_name/end_name as computed keys in API

Instead of a separate node, _get_data now appends three derived keys
to every sequence response: Path(start frame path).stem → start_name,
etc. Any ProjectKey node can use these directly as key_name.

Reverts ProjectFrameNames node (unnecessary).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-04 12:19:12 +02:00
parent f857485bc8
commit 111b37dc8d
3 changed files with 10 additions and 185 deletions
+10 -2
View File
@@ -83,9 +83,17 @@ def _get_data(name: str, file_name: str, seq: int = Query(default=1)) -> dict[st
match = next((s for s in sequences if int(s.get(KEY_SEQUENCE_NUMBER, 0)) == seq), None) match = next((s for s in sequences if int(s.get(KEY_SEQUENCE_NUMBER, 0)) == seq), None)
if match is None: if match is None:
raise HTTPException(status_code=404, detail=f"Sequence {seq} not found") raise HTTPException(status_code=404, detail=f"Sequence {seq} not found")
result = dict(match)
for out_key, src_key in (
("start_name", "start frame path"),
("middle_name", "middle frame path"),
("end_name", "end frame path"),
):
path_val = result.get(src_key, "")
result[out_key] = Path(path_val).stem if path_val else ""
logger.info("API _get_data %s/%s seq=%d (%d keys): %.3fs", logger.info("API _get_data %s/%s seq=%d (%d keys): %.3fs",
name, file_name, seq, len(match), time.perf_counter() - t0) name, file_name, seq, len(result), time.perf_counter() - t0)
return match return result
def _get_keys(name: str, file_name: str, seq: int = Query(default=1)) -> dict[str, Any]: def _get_keys(name: str, file_name: str, seq: int = Query(default=1)) -> dict[str, Any]:
-52
View File
@@ -383,63 +383,12 @@ class BinaryIndexDecoder:
) )
class ProjectFrameNames:
"""Outputs the filename stem of each frame path field (no directory, no extension).
Fetches start frame path, middle frame path, and end frame path from the
sequence data and returns Path(value).stem for each, so you get e.g.
'keyframe8' instead of '/some/dir/keyframe8.png'.
"""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"source_label": ("STRING", {"default": "", "multiline": False}),
},
"optional": {
"manager_url": ("STRING", {"default": "http://localhost:8080", "multiline": False}),
"project_name": ("STRING", {"default": "", "multiline": False}),
"file_name": ("STRING", {"default": "", "multiline": False}),
"sequence_number": ("INT", {"default": 1, "min": 1, "max": 9999}),
},
}
RETURN_TYPES = ("STRING", "STRING", "STRING")
RETURN_NAMES = ("start_name", "middle_name", "end_name")
FUNCTION = "fetch_frame_names"
CATEGORY = "JSON Manager/project"
OUTPUT_NODE = False
@classmethod
def IS_CHANGED(cls, **kwargs):
return float("nan")
def fetch_frame_names(self, source_label, manager_url="http://localhost:8080",
project_name="", file_name="", sequence_number=1):
sequence_number = int(sequence_number)
data = _fetch_data(manager_url, project_name, file_name, sequence_number)
if data.get("error") in ("http_error", "network_error", "parse_error"):
logger.warning("ProjectFrameNames.fetch_frame_names failed: %s", data.get("message"))
return ("", "", "")
def stem(path_str):
return Path(path_str).stem if path_str else ""
return (
stem(data.get("start frame path", "")),
stem(data.get("middle frame path", "")),
stem(data.get("end frame path", "")),
)
# --- Mappings --- # --- Mappings ---
PROJECT_NODE_CLASS_MAPPINGS = { PROJECT_NODE_CLASS_MAPPINGS = {
"ProjectLoaderDynamic": ProjectLoaderDynamic, "ProjectLoaderDynamic": ProjectLoaderDynamic,
"ProjectSource": ProjectSource, "ProjectSource": ProjectSource,
"ProjectKey": ProjectKey, "ProjectKey": ProjectKey,
"ProjectResolution": ProjectResolution, "ProjectResolution": ProjectResolution,
"ProjectFrameNames": ProjectFrameNames,
"BinaryIndexDecoder": BinaryIndexDecoder, "BinaryIndexDecoder": BinaryIndexDecoder,
} }
@@ -448,6 +397,5 @@ PROJECT_NODE_DISPLAY_NAME_MAPPINGS = {
"ProjectSource": "Project Source", "ProjectSource": "Project Source",
"ProjectKey": "Project Key", "ProjectKey": "Project Key",
"ProjectResolution": "Project Resolution", "ProjectResolution": "Project Resolution",
"ProjectFrameNames": "Project Frame Names",
"BinaryIndexDecoder": "Binary Index Decoder", "BinaryIndexDecoder": "Binary Index Decoder",
} }
-131
View File
@@ -1,131 +0,0 @@
import { app } from "../../scripts/app.js";
app.registerExtension({
name: "json.manager.project.frame_names",
async beforeQueuePrompt() {
if (!app.graph?._nodes) return;
for (const node of app.graph._nodes) {
if (node.type === "ProjectFrameNames" && node._syncFromSource) {
node._syncFromSource();
}
}
},
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name !== "ProjectFrameNames") return;
function hideWidget(widget) {
if (widget.origType === undefined) widget.origType = widget.type;
widget.type = "hidden";
widget.hidden = true;
widget.computeSize = () => [0, -4];
}
function replaceWithCombo(node, name, values, callback) {
const idx = node.widgets?.findIndex(w => w.name === name);
if (idx === -1 || idx === undefined) return null;
const oldWidget = node.widgets[idx];
const savedValue = oldWidget.value || "";
const comboValues = values.length > 0 ? values : [""];
if (savedValue && !comboValues.includes(savedValue)) comboValues.unshift(savedValue);
const defaultValue = savedValue || comboValues[0];
node.widgets.splice(idx, 1);
const combo = node.addWidget("combo", name, defaultValue, callback, { values: comboValues });
if (node.widgets.length > 1) {
node.widgets.splice(node.widgets.length - 1, 1);
node.widgets.splice(idx, 0, combo);
}
return combo;
}
nodeType.prototype._getSourceLabels = function () {
const seen = new Set();
const labels = [];
if (!this.graph) return labels;
for (const node of this.graph._nodes) {
if (node.type === "ProjectSource") {
const lw = node.widgets?.find(w => w.name === "label");
if (lw?.value && !seen.has(lw.value)) {
seen.add(lw.value);
labels.push(lw.value);
}
}
}
return labels;
};
nodeType.prototype._findSource = function (label) {
if (!this.graph || !label) return null;
for (const node of this.graph._nodes) {
if (node.type === "ProjectSource") {
const lw = node.widgets?.find(w => w.name === "label");
if (lw?.value === label) return node;
}
}
return null;
};
nodeType.prototype._syncFromSource = function () {
const srcWidget = this.widgets?.find(w => w.name === "source_label");
const source = this._findSource(srcWidget?.value);
if (!source) return;
for (const name of ["manager_url", "project_name", "file_name", "sequence_number"]) {
const dst = this.widgets?.find(w => w.name === name);
const src = source.widgets?.find(w => w.name === name);
if (dst && src) dst.value = src.value;
}
};
const origOnNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
origOnNodeCreated?.apply(this, arguments);
for (const name of ["manager_url", "project_name", "file_name", "sequence_number"]) {
const w = this.widgets?.find(w => w.name === name);
if (w) hideWidget(w);
}
const node = this;
replaceWithCombo(this, "source_label", this._getSourceLabels?.() || [], function () {
node._syncFromSource();
});
this.title = "Project Frame Names";
this.setSize(this.computeSize());
};
const origOnConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function (info) {
origOnConfigure?.apply(this, arguments);
for (const name of ["manager_url", "project_name", "file_name", "sequence_number"]) {
const w = this.widgets?.find(w => w.name === name);
if (w) hideWidget(w);
}
const srcWidget = this.widgets?.find(w => w.name === "source_label");
if (srcWidget && srcWidget.type !== "combo") {
const node = this;
replaceWithCombo(this, "source_label", this._getSourceLabels?.() || [], function () {
node._syncFromSource();
});
} else if (srcWidget) {
srcWidget.options.values = this._getSourceLabels?.() || [];
}
this.setSize(this.computeSize());
const node = this;
queueMicrotask(() => node._syncFromSource());
};
const origOnMouseDown = nodeType.prototype.onMouseDown;
nodeType.prototype.onMouseDown = function (e, localPos, graphCanvas) {
origOnMouseDown?.apply(this, arguments);
const srcWidget = this.widgets?.find(w => w.name === "source_label");
if (srcWidget) srcWidget.options.values = this._getSourceLabels?.() || [];
this._syncFromSource();
};
},
});