Add persistent any text preview node
This commit is contained in:
@@ -17,6 +17,7 @@ The node is registered as:
|
||||
- `prompt_builder / SxCP For Loop End`
|
||||
- `prompt_builder / SxCP Loop Append`
|
||||
- `prompt_builder / SxCP Accumulator`
|
||||
- `prompt_builder / util / SxCP Preview Any As Text`
|
||||
- `prompt_builder / SxCP Category Preset`
|
||||
- `prompt_builder / SxCP Cast Control`
|
||||
- `prompt_builder / SxCP Cast Bias`
|
||||
@@ -170,6 +171,13 @@ Its outputs are:
|
||||
ComfyUI image batches require matching dimensions. For mixed image formats, use
|
||||
`image_list` or the grouped `image_batch_1..4` outputs instead of `image_batch`.
|
||||
|
||||
`SxCP Preview Any As Text` is a persistent text preview for arbitrary values.
|
||||
Connect any output to `value`; the node renders strings directly and formats
|
||||
dict/list/tensor-like values as readable text. After execution, its
|
||||
`preview_text` widget is updated by the frontend and is serialized in the
|
||||
workflow. Save the workflow after a run and the preview text will still be
|
||||
there after reload.
|
||||
|
||||
## Character Profiles
|
||||
|
||||
`SxCP Woman Slot` and `SxCP Man Slot` are the scalable per-participant control
|
||||
|
||||
@@ -153,6 +153,9 @@ COMMON_INPUT_TOOLTIPS = {
|
||||
"save_path": "Folder to save the accumulator batch. Relative paths are inside ComfyUI output; absolute paths are used directly.",
|
||||
"filename_prefix": "Filename prefix for saved accumulator images.",
|
||||
"clear_after_save": "Clear the accumulator store after a successful batch save.",
|
||||
"preview_text": "Serialized persistent text preview. It is updated after execution and saved with the workflow.",
|
||||
"preview_format": "How to convert an arbitrary input to preview text.",
|
||||
"max_chars": "Maximum stored preview characters. 0 disables truncation.",
|
||||
"mode": "Switch direction: pick_input selects one input to value, route_output sends route_value to one output.",
|
||||
"index": "Index used by SxCP Index Switch. For Loop Start outputs one_based indexes by default.",
|
||||
"index_base": "one_based means index 1 selects input_1. zero_based means index 0 selects input_1.",
|
||||
|
||||
+102
@@ -50,6 +50,7 @@ ACCUMULATOR_PREVIEW_DELETE_ACTIONS = ["none", "delete_entry_id", "delete_index",
|
||||
INDEX_SWITCH_MODES = ["pick_input", "route_output"]
|
||||
INDEX_SWITCH_BASES = ["one_based", "zero_based"]
|
||||
INDEX_SWITCH_MISSING_BEHAVIORS = ["fallback", "none", "clamp", "wrap"]
|
||||
PREVIEW_TEXT_FORMATS = ["auto", "json", "repr", "str"]
|
||||
|
||||
_ACCUMULATOR_STORES: dict[str, list[dict[str, Any]]] = {}
|
||||
|
||||
@@ -248,6 +249,66 @@ def _attach_preview_images(entries: list[dict[str, Any]], images: list[dict[str,
|
||||
entry["preview_image"] = image
|
||||
|
||||
|
||||
def _jsonable_preview_value(value: Any, depth: int = 4, max_items: int = 80) -> Any:
|
||||
if depth < 0:
|
||||
return "..."
|
||||
if value is None or isinstance(value, (str, int, float, bool)):
|
||||
return value
|
||||
if isinstance(value, dict):
|
||||
items = list(value.items())
|
||||
output = {
|
||||
str(key): _jsonable_preview_value(item, depth - 1, max_items)
|
||||
for key, item in items[:max_items]
|
||||
}
|
||||
if len(items) > max_items:
|
||||
output["..."] = f"{len(items) - max_items} more"
|
||||
return output
|
||||
if isinstance(value, (list, tuple, set)):
|
||||
items = list(value)
|
||||
output = [_jsonable_preview_value(item, depth - 1, max_items) for item in items[:max_items]]
|
||||
if len(items) > max_items:
|
||||
output.append(f"... {len(items) - max_items} more")
|
||||
return output
|
||||
shape = getattr(value, "shape", None)
|
||||
if shape is not None:
|
||||
try:
|
||||
return {
|
||||
"type": type(value).__name__,
|
||||
"shape": [int(part) for part in shape],
|
||||
"dtype": str(getattr(value, "dtype", "")),
|
||||
"device": str(getattr(value, "device", "")),
|
||||
}
|
||||
except Exception:
|
||||
pass
|
||||
return str(value)
|
||||
|
||||
|
||||
def _truncate_preview_text(text: str, max_chars: int) -> str:
|
||||
max_chars = max(0, int(max_chars or 0))
|
||||
if max_chars <= 0 or len(text) <= max_chars:
|
||||
return text
|
||||
omitted = len(text) - max_chars
|
||||
return f"{text[:max_chars]}\n... truncated {omitted} characters"
|
||||
|
||||
|
||||
def _any_to_preview_text(value: Any, preview_format: str, max_chars: int) -> str:
|
||||
preview_format = preview_format if preview_format in PREVIEW_TEXT_FORMATS else "auto"
|
||||
if preview_format == "str":
|
||||
text = str(value)
|
||||
elif preview_format == "repr":
|
||||
text = repr(value)
|
||||
elif preview_format == "json":
|
||||
text = json.dumps(_jsonable_preview_value(value), ensure_ascii=True, indent=2, sort_keys=True)
|
||||
elif isinstance(value, str):
|
||||
text = value
|
||||
else:
|
||||
try:
|
||||
text = json.dumps(_jsonable_preview_value(value), ensure_ascii=True, indent=2, sort_keys=True)
|
||||
except Exception:
|
||||
text = str(value)
|
||||
return _truncate_preview_text(text, max_chars)
|
||||
|
||||
|
||||
def _accumulator_status(key: str, store: list[dict[str, Any]]) -> str:
|
||||
images = [entry.get("image") for entry in store if entry.get("image") is not None]
|
||||
shapes = []
|
||||
@@ -1194,6 +1255,45 @@ class SxCPAccumulatorPreview:
|
||||
}
|
||||
|
||||
|
||||
class SxCPPreviewAnyAsText:
|
||||
OUTPUT_NODE = True
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"preview_text": ("STRING", {"default": "", "multiline": True}),
|
||||
"preview_format": (PREVIEW_TEXT_FORMATS, {"default": "auto"}),
|
||||
"max_chars": ("INT", {"default": 20000, "min": 0, "max": 200000, "step": 1000}),
|
||||
},
|
||||
"optional": {
|
||||
"value": (ANY_TYPE, {"forceInput": True}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("text",)
|
||||
FUNCTION = "preview"
|
||||
CATEGORY = "prompt_builder/util"
|
||||
|
||||
@classmethod
|
||||
def IS_CHANGED(cls, *args, **kwargs):
|
||||
return random.random()
|
||||
|
||||
def preview(self, preview_text, preview_format, max_chars, **kwargs):
|
||||
if "value" not in kwargs:
|
||||
text = _truncate_preview_text(str(preview_text or ""), int(max_chars))
|
||||
else:
|
||||
value = kwargs.get("value")
|
||||
text = _any_to_preview_text(value, str(preview_format or "auto"), int(max_chars))
|
||||
return {
|
||||
"ui": {
|
||||
"preview_text": [text],
|
||||
},
|
||||
"result": (text,),
|
||||
}
|
||||
|
||||
|
||||
class SxCPForLoopEnd:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
@@ -1320,6 +1420,7 @@ LOOP_NODE_CLASS_MAPPINGS = {
|
||||
"SxCPIndexSwitch": SxCPIndexSwitch,
|
||||
"SxCPAccumulator": SxCPAccumulator,
|
||||
"SxCPAccumulatorPreview": SxCPAccumulatorPreview,
|
||||
"SxCPPreviewAnyAsText": SxCPPreviewAnyAsText,
|
||||
"SxCPLoopIntAdd": SxCPLoopIntAdd,
|
||||
"SxCPLoopLessThan": SxCPLoopLessThan,
|
||||
"SxCPLoopLessThanOrEqual": SxCPLoopLessThanOrEqual,
|
||||
@@ -1334,6 +1435,7 @@ LOOP_NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"SxCPIndexSwitch": "SxCP Index Switch",
|
||||
"SxCPAccumulator": "SxCP Accumulator",
|
||||
"SxCPAccumulatorPreview": "SxCP Accumulator Preview",
|
||||
"SxCPPreviewAnyAsText": "SxCP Preview Any As Text",
|
||||
"SxCPLoopIntAdd": "SxCP Loop Int Add",
|
||||
"SxCPLoopLessThan": "SxCP Loop Less Than",
|
||||
"SxCPLoopLessThanOrEqual": "SxCP Loop Less Than Or Equal",
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js";
|
||||
|
||||
const EXTENSION = "ethanfel.prompt_builder.preview_any_text";
|
||||
const NODE_NAME = "SxCPPreviewAnyAsText";
|
||||
|
||||
function isPreviewTextNode(node) {
|
||||
return node?.comfyClass === NODE_NAME || node?.type === NODE_NAME;
|
||||
}
|
||||
|
||||
function getNodeById(id) {
|
||||
return app.graph?.getNodeById?.(Number(id)) || app.graph?._nodes_by_id?.[id] || app.graph?._nodes_by_id?.[Number(id)];
|
||||
}
|
||||
|
||||
function widget(node, name) {
|
||||
return node.widgets?.find((item) => item.name === name);
|
||||
}
|
||||
|
||||
function outputPreviewText(output) {
|
||||
const raw = output?.preview_text;
|
||||
if (Array.isArray(raw)) return raw[0] ?? "";
|
||||
return raw ?? "";
|
||||
}
|
||||
|
||||
function setPreviewWidget(node, text) {
|
||||
const preview = widget(node, "preview_text");
|
||||
if (!preview) return;
|
||||
preview.value = text;
|
||||
if (preview.inputEl) preview.inputEl.value = text;
|
||||
preview.callback?.(text, app.canvas, node);
|
||||
node.setDirtyCanvas?.(true, true);
|
||||
app.graph?.setDirtyCanvas?.(true, true);
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: EXTENSION,
|
||||
|
||||
async setup() {
|
||||
api.addEventListener("executed", ({detail}) => {
|
||||
const node = getNodeById(detail?.display_node ?? detail?.node);
|
||||
if (!isPreviewTextNode(node)) return;
|
||||
const text = outputPreviewText(detail?.output || {});
|
||||
setPreviewWidget(node, text);
|
||||
});
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user