Commit 2a934cec authored by raojy's avatar raojy
Browse files

first

parent 4b618aa3
{
"id": "a92af27a-0106-4c6f-9d1c-f9783b652f44",
"revision": 0,
"last_node_id": 5,
"last_link_id": 3,
"nodes": [
{
"id": 1,
"type": "SenseNovaU1LocalLoader",
"pos": [
-535.8793125610355,
146.00520475769034
],
"size": [
504,
432
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "u1_model",
"type": "SENSENOVA_U1_LOCAL_MODEL",
"links": [
1
]
},
{
"name": "model_info_json",
"type": "STRING",
"links": null
}
],
"properties": {
"Node name for S&R": "SenseNovaU1LocalLoader"
},
"widgets_values": [
"sensenova/SenseNova-U1-8B-MoT",
"",
"cuda",
"bfloat16",
"auto",
"none",
"",
"full",
""
]
},
{
"id": 2,
"type": "SenseNovaU1LocalTextToImage",
"pos": [
2.00001409912079,
146.00000553894034
],
"size": [
565.953125,
887.203125
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "u1_model",
"type": "SENSENOVA_U1_LOCAL_MODEL",
"link": 1
}
],
"outputs": [
{
"name": "images",
"type": "IMAGE",
"links": [
2
]
},
{
"name": "text",
"type": "STRING",
"links": null
},
{
"name": "think_text",
"type": "STRING",
"links": [
3
]
},
{
"name": "metadata_json",
"type": "STRING",
"links": null
}
],
"properties": {
"Node name for S&R": "SenseNovaU1LocalTextToImage"
},
"widgets_values": [
"这张信息图的标题是“SenseNova-U1”,采用现代极简科技矩阵风格。整体布局为水平三列网格结构,背景是带有极浅银灰色细密点阵的哑光纯白高级纸张纹理,画面长宽比为16:9。\\n\\n排版采用严谨的视觉层级:主标题使用粗体无衬线黑体字,正文使用清晰的现代等宽字体。配色方案极其克制,以纯白色为底,深炭黑为主视觉文字和边框,浅石板灰用于背景色块和次要信息区分,图标采用精致的银灰色线框绘制。\\n\\n在画面正上方居中位置,使用醒目的深炭黑粗体字排布着大标题“SenseNova-U1”。标题正下方是浅石板灰色的等宽字体副标题“新一代端到端统一多模态大模型家族”。\\n\\n画面主体分为左、中、右三个相等的垂直信息区块,区块之间通过充足的负空间进行物理隔离。\\n\\n左侧区块的主题是概述。顶部有一个银灰色线框绘制的、由放大镜和齿轮交织的图标,旁边是粗体小标题“Overview”。该区块内从上到下垂直排列着三个要点:第一个要点旁边是一个代表文档与照片重叠的极简图标,紧跟着文字“多模态模型家族,统一文本/图像理解和生成”。向下是由两个相连的同心圆组成的架构图标,配有文字“基于NEO-Unify架构(端到端统一理解和生成)”。最下方是一个带有斜线划掉的眼睛和漏斗形状的图标,明确指示文本“无需视觉编码器(VE)和变分自编码器(VAE)”。\\n\\n中间区块展示模型矩阵。顶部是一个包含两个分支节点的树状网络图标,旁边是粗体小标题“两个模型规格”。区块内分为上下两个包裹在浅石板灰色极细边框内的卡片。上方的卡片内画着一个代表高密度的实心几何立方体图标,大字标注“SenseNova-U1-8B-MoT”,下方是等宽字体说明“8B MoT 密集主干模型”。下方的卡片内画着一个带有闪电符号的网状发光大脑图标,大字标注“SenseNova-U1-A3B-MoT”,下方是等宽字体说明“A3B MoT 混合专家(MoE)主干模型”。在这两个独立卡片的正下方,左侧放置一个笑脸轮廓图标搭配文字“将在HF等平台公开”,右侧放置一个带有折角的书面报告图标搭配文字“将发布技术报告”。\\n\\n右侧区块呈现核心优势。顶部是一个代表巅峰的上升阶梯折线图图标,旁边是粗体小标题“Highlights”。该区块内部垂直分布着四个带有浅石板灰底色的长方形色块,每个色块内部左侧对应一个具体的图标,右侧为文字。第一个色块内是一个无缝相连的莫比乌斯环图标,配文“原生统一架构,无VE和VAE”。第二个色块内是一个顶端带有星星的奖杯图标,配文“单一统一模型在理解和生成任务上均达到SOTA性能”。第三个色块内是代表文本行与拍立得照片交替穿插的图标,配文“强大的原生交错推理能力(模型原生生成图像进行推理)”。最后一个色块内是一个被切分出一小块的硬币与详细饼状图结合的图标,配文“能生成复杂信息图表,性价比出色”。",
"2720x1536|16:9",
4,
"none",
3,
0,
1,
50,
1,
42,
false,
false
]
},
{
"id": 3,
"type": "PreviewImage",
"pos": [
680.5872600708007,
135.8365686645507
],
"size": [
614.59375,
393.609375
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 2
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewImage"
},
"widgets_values": []
},
{
"id": 4,
"type": "PreviewAny",
"pos": [
684.4660397338865,
621.6779303283691
],
"size": [
616.296875,
241
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "source",
"type": "*",
"link": 3
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewAny"
},
"widgets_values": [
null,
null,
null
]
}
],
"links": [
[
1,
1,
0,
2,
0,
"SENSENOVA_U1_LOCAL_MODEL"
],
[
2,
2,
0,
3,
0,
"IMAGE"
],
[
3,
2,
2,
4,
0,
"STRING"
]
],
"groups": [],
"config": {},
"extra": {
"workflowRendererVersion": "Vue",
"ds": {
"scale": 0.7513148009015777,
"offset": [
736.3823906250004,
23.45817187500008
]
},
"frontendVersion": "1.39.19",
"VHS_latentpreview": false,
"VHS_latentpreviewrate": 0,
"VHS_MetadataImage": true,
"VHS_KeepIntermediate": true
},
"version": 0.4
}
from __future__ import annotations
import base64
import binascii
from io import BytesIO
from urllib.parse import urlparse
import numpy as np
from PIL import Image
MAX_IMAGE_BYTES = 50 * 1024 * 1024
def is_http_url(value: str) -> bool:
parsed = urlparse(value)
return parsed.scheme in {"http", "https"} and bool(parsed.netloc)
def strip_data_url(value: str) -> str:
if value.startswith("data:") and "," in value:
return value.split(",", 1)[1]
return value
def is_image_data_url(value: str) -> bool:
return value.startswith("data:image/") and ";base64," in value
def is_supported_vision_image_url(value: str) -> bool:
return is_http_url(value) or is_image_data_url(value)
def decode_base64_image(value: str) -> Image.Image:
try:
data = base64.b64decode(strip_data_url(value), validate=True)
except (binascii.Error, ValueError) as exc:
raise RuntimeError("Image response contains invalid base64 data.") from exc
return image_from_bytes(data)
def image_from_bytes(data: bytes) -> Image.Image:
if len(data) > MAX_IMAGE_BYTES:
raise RuntimeError("Image response is larger than the 50MB safety limit.")
try:
with Image.open(BytesIO(data)) as image:
return image.convert("RGB")
except Exception as exc:
raise RuntimeError("Image response could not be decoded by Pillow.") from exc
def pil_to_comfy_image(image: Image.Image):
try:
import torch
except ImportError as exc:
raise RuntimeError("PyTorch is required by ComfyUI to output IMAGE tensors.") from exc
rgb_image = image.convert("RGB")
array = np.array(rgb_image, dtype=np.float32, copy=True) / 255.0
array = np.ascontiguousarray(array)
tensor = torch.from_numpy(array).unsqueeze(0)
return tensor.contiguous().float()
def image_bytes_to_comfy_image(data: bytes):
return pil_to_comfy_image(image_from_bytes(data))
def comfy_image_info(image) -> str:
shape = tuple(image.shape) if hasattr(image, "shape") else "<unknown>"
dtype = getattr(image, "dtype", "<unknown>")
device = getattr(image, "device", "<unknown>")
is_contiguous = image.is_contiguous() if hasattr(image, "is_contiguous") else "<unknown>"
try:
min_value = float(image.min())
max_value = float(image.max())
value_range = f"{min_value:.6f}..{max_value:.6f}"
except Exception:
value_range = "<unknown>"
return f"shape={shape}; dtype={dtype}; device={device}; contiguous={is_contiguous}; range={value_range}"
def comfy_image_to_pil(image) -> Image.Image:
if hasattr(image, "detach"):
image = image.detach().cpu().numpy()
array = np.asarray(image)
if array.ndim == 4:
if array.shape[0] < 1:
raise RuntimeError("ComfyUI IMAGE batch is empty.")
array = array[0]
if array.ndim != 3 or array.shape[-1] not in {3, 4}:
raise RuntimeError("ComfyUI IMAGE must have shape [B,H,W,C] or [H,W,C].")
array = np.clip(array, 0.0, 1.0)
array = (array * 255.0).round().astype(np.uint8)
return Image.fromarray(array).convert("RGB")
def comfy_batch_to_pil_images(image) -> list[Image.Image]:
if hasattr(image, "detach"):
image = image.detach().cpu().numpy()
array = np.asarray(image)
if array.ndim == 3:
array = array[None, ...]
if array.ndim != 4 or array.shape[-1] not in {3, 4}:
raise RuntimeError("ComfyUI IMAGE batch must have shape [B,H,W,C].")
array = np.clip(array, 0.0, 1.0)
array = (array * 255.0).round().astype(np.uint8)
return [Image.fromarray(item).convert("RGB") for item in array]
def pil_to_png_data_url(image: Image.Image) -> str:
buffer = BytesIO()
image.convert("RGB").save(buffer, format="PNG")
encoded = base64.b64encode(buffer.getvalue()).decode("ascii")
return f"data:image/png;base64,{encoded}"
def comfy_image_to_png_data_url(image) -> str:
return pil_to_png_data_url(comfy_image_to_pil(image))
from __future__ import annotations
import argparse
import os
import shutil
import subprocess
import sys
from pathlib import Path
DEFAULT_NODE_NAME = "ComfyUI-SenseNova-U1"
def _link_or_junction(source: Path, target: Path) -> str:
"""Create a directory symlink, falling back to an NTFS junction on Windows.
Windows blocks `os.symlink` unless the user is an administrator or
Developer Mode is enabled (WinError 1314). A directory junction
(`mklink /J`) provides the same `Path.resolve()`-followable semantics
without any privilege; ComfyUI loads through it and the loader's
auto-discovery still finds the monorepo source.
"""
try:
os.symlink(source, target, target_is_directory=True)
return "Linked"
except OSError as exc:
if sys.platform != "win32":
raise
try:
subprocess.check_call(
["cmd", "/c", "mklink", "/J", str(target), str(source)],
stdout=subprocess.DEVNULL,
)
return "Junctioned"
except (subprocess.CalledProcessError, FileNotFoundError) as junc_exc:
raise SystemExit(
f"Could not create a symlink at {target}: {exc}\n"
f"Falling back to `mklink /J` also failed: {junc_exc}\n"
"Re-run with --copy, enable Windows Developer Mode "
"(Settings → For Developers), or run this script as Administrator."
) from exc
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Install the SenseNova-U1 ComfyUI app.")
parser.add_argument(
"--comfyui",
required=True,
help="Path to the ComfyUI checkout that contains the custom_nodes directory.",
)
parser.add_argument(
"--name",
default=DEFAULT_NODE_NAME,
help=f"Directory name under ComfyUI/custom_nodes (default: {DEFAULT_NODE_NAME}).",
)
parser.add_argument(
"--copy",
action="store_true",
help="Copy files instead of creating a symlink.",
)
parser.add_argument(
"--install-deps",
action="store_true",
help="Run pip install -r requirements.txt with the current Python.",
)
parser.add_argument(
"--force",
action="store_true",
help="Replace an existing symlink or directory at the target path.",
)
return parser.parse_args()
def main() -> None:
args = parse_args()
app_dir = Path(__file__).resolve().parent
repo_dir = app_dir.parents[1]
comfyui_dir = Path(args.comfyui).expanduser().resolve()
custom_nodes = comfyui_dir / "custom_nodes"
target = custom_nodes / args.name
if not custom_nodes.is_dir():
raise SystemExit(f"ComfyUI custom_nodes directory not found: {custom_nodes}")
if target.exists() or target.is_symlink():
if not args.force:
raise SystemExit(
f"Target already exists: {target}\nRe-run with --force to replace it, or choose another --name."
)
if target.is_symlink() or target.is_file():
target.unlink()
else:
shutil.rmtree(target)
if args.copy:
shutil.copytree(app_dir, target, ignore=shutil.ignore_patterns("__pycache__"))
action = "Copied"
else:
action = _link_or_junction(app_dir, target)
print(f"{action} SenseNova-U1 ComfyUI app:")
print(f" {target} -> {app_dir}")
if not args.copy:
# Default symlink (or Windows junction) mode: local_pipeline.py's
# default_source_path() resolves __file__ through the link back to
# this monorepo and discovers <repo>/src automatically. No env var
# needed for local inference.
print(f"\n{action} mode: SENSENOVA_U1_SRC auto-resolves to")
print(f" {repo_dir / 'src'}")
print("via local_pipeline.default_source_path(), because the loader")
print("file is linked back into this checkout. Moving or renaming")
print("the monorepo breaks that link — re-run install.py afterwards.")
else:
# --copy mode: files live under <ComfyUI>/custom_nodes/, no symlink
# to follow, so the user must point SENSENOVA_U1_SRC explicitly.
print("\nCopy mode: auto-discovery is disabled (no symlink to follow).")
print(f"Set SENSENOVA_U1_SRC={repo_dir / 'src'}")
print("in the ComfyUI launch environment, or fill the loader node's")
print("`sensenova_u1_src` input.")
if args.install_deps:
requirements = app_dir / "requirements.txt"
subprocess.check_call([sys.executable, "-m", "pip", "install", "-r", str(requirements)])
print("\nFor local inference, also install the SenseNova-U1 runtime in the ComfyUI Python environment:")
print(f" {sys.executable} -m pip install -e {repo_dir}")
print(" Restart ComfyUI.")
else:
print("\nNext steps:")
print(f" {sys.executable} -m pip install -r {app_dir / 'requirements.txt'}")
print(f" {sys.executable} -m pip install -e {repo_dir} # for local inference")
print(" Restart ComfyUI.")
if __name__ == "__main__":
main()
This diff is collapsed.
This diff is collapsed.
from __future__ import annotations
from pathlib import Path
PROMPTS_DIR = Path(__file__).resolve().parent / "prompts"
def load_prompt_template(filename: str) -> str:
path = PROMPTS_DIR / filename
try:
return path.read_text(encoding="utf-8").strip()
except FileNotFoundError as exc:
raise RuntimeError(f"Prompt template not found: {path}") from exc
You are a world-renowned "Senior Visual Information Architect" and "AI Image Prompt Engineering Expert." You specialize in transforming fragmented or chaotic [Raw Information] into highly structured, professional Infographic Generation Prompts. Your work is defined by rigorous visual logic, precise spatial organization, and an density of useful information.
# Task
Reconstruct the user’s [Raw Information] into a comprehensive visual synthesis prompt (approx. 400-600 words). Your objective is to guide large image models (e.g., Gemini, Midjourney, DALL-E 3) to render an information-dense infographic featuring advanced typography, a vivid visual style, and perfect structural clarity based solely on your textual description.
# Step-by-Step Methodology
1. **Content Expansion & Textualization**: Analyze the [Raw Information] to extract its core intent.
- Detailing: Extract every entity, number, color, and phrase from the [Raw Information]. Do not summarize.
- Categorization: Define sub-categories with distinct visual markers.
- Density Enrichment: If the input is brief, supplement it with professional annotations, sub-headings, body text and "Pro-tips" or "Key Insights" related to the topic to maximize the "information load".
2. **Adaptive Structural Analysis**:
- User-Defined Priority: First, check if the user has provided specific layout instructions (e.g., "three-column grid," "horizontal timeline"). If present, strictly follow these instructions.
- Logic-Driven Inference: If no layout is specified, analyze the [Raw Information] for its underlying logic (chronological, hierarchical, process-oriented, or comparative) and design a spatial architecture that best serves that logic.
3. **Style Tonal Setting**: If no specific style is provided, assign a unique aesthetic that complements the content (e.g., French hand-drawn collage, modern minimalist matrix, or industrial technical blueprint).
4. **Data Preservation & Encoding**: Ensure all numbers, dates, and proper nouns are 100% preserved. Convert these into explicit visual labels, charts, or callouts within the prompt. Detect the language of the [Raw Information] and use it for 100% of the output. If input is Chinese, output Chinese. If input is English, output English. No mixing.
# Strict Constraints
1. **Strict Language Parity**: Maintain absolute language consistency. If the [Raw Information] is in Chinese, the entire output must be in Chinese; if in English, the output must be in English. No code-switching.
2. **Fidelity to [Raw Information]**: You are prohibited from omitting any proper nouns, dates, colors, or specific values provided in the input.
3. **The "Zero Nonsense" Rule**: STRICTLY FORBIDDEN to include introductory, summary, or meta-commentary text (e.g., "Here is the refined prompt..."). Do not explain design choices or justify element omissions (e.g., do not mention "implied flow"). Start the response immediately with the visual description.
4. **Visual Precision:
- Textures: Mandatorily describe background textures (e.g., off-white aged paper, light gray grid, or black halftone shadows).
- Typography: Explicitly specify font styles for different hierarchies (e.g., bold serif for titles, condensed mono-space for technical data).
5. **Text Rendering Protocol**:
- Quotes for Content: Every piece of text intended to appear in the image MUST be enclosed in quotes.
- No Quotes for Style: NEVER use quotation marks for descriptions of [Style Description], [Layout Structure], colors or any non-textual elements.
6. **Relational Arrow Logic**: Minimize the use of arrows. Rely on spatial proximity or alignment to imply connectivity. If arrows are requested, avoid generic orientations like "horizontal." Instead, specify their precise starting point and target destination.
7. **Semantic Icon Correspondence (CRITICAL)**: You must specifically describe the visual content of every icon to ensure it matches the quoted text. (e.g., "Next to the text 'Apple' is a detailed illustration of a red delicious apple with a green leaf.") Do not use generic terms like "an icon" or "a graphic" without specifying what it is.
8. **No Hexadecimal Codes**: Never use codes like #xxxx. Use descriptive color names (e.g., sage green, deep navy blue, terracotta).
# Output Format (If the [Raw Information] is in Chinese, please translate the following content into Chinese. If the [Raw Information] is in English, please keep the following content in English.)
The theme of the infographic is [Subject Name] (or 此信息图的主题是: [Subject Name]), [Style Description]. The overall layout is [Layout Structure], with a background of [Background Details].
Provide a smooth and fluent description of the prompts for generating professional infographics. The title is: "Subject Name", [Description of elements or icons in the infographic], [Position], and embed the text information within it, enclosed in quotes.
---
Please receive the user's [Raw Information] and directly output the restructured professional image generation prompt:
# Metadata read by `Comfy-Org/publish-node-action` when this directory is
# published to https://registry.comfy.org as the standalone repository
# `OpenSenseNova/ComfyUI-SenseNova-U1`. The SenseNova-U1 monorepo's build
# (hatchling) ignores this file — see ../../pyproject.toml.
#
# Intentionally minimal: no [tool.ruff] / [tool.pytest] / [build-system]
# sections, so the monorepo's root configuration continues to apply to files
# under this directory.
[project]
name = "ComfyUI-SenseNova-U1"
version = "0.1.4"
description = "SenseNova-U1 custom nodes for ComfyUI (API + local inference)."
readme = "README.md"
license = { file = "LICENSE" }
requires-python = ">=3.10"
authors = [{ name = "OpenSenseNova" }]
classifiers = [
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python :: 3",
]
dependencies = [
"httpx",
"numpy",
"pillow",
"python-dotenv",
# Tarball URL (not git+https) so pip skips submodule init; see
# requirements.txt for the full rationale. Bump to /tags/vX.Y.Z.tar.gz
# before each official Comfy Registry publish for reproducibility.
"sensenova-u1 @ https://github.com/OpenSenseNova/SenseNova-U1/archive/refs/heads/main.tar.gz",
]
[project.urls]
Homepage = "https://github.com/OpenSenseNova/ComfyUI-SenseNova-U1"
Repository = "https://github.com/OpenSenseNova/ComfyUI-SenseNova-U1"
Source = "https://github.com/OpenSenseNova/SenseNova-U1/tree/main/apps/comfyui"
[tool.comfy]
PublisherId = "sensenova"
DisplayName = "ComfyUI-SenseNova-U1"
Icon = ""
httpx
numpy
pillow
python-dotenv
# Local inference path. Use a GitHub-generated tarball, not git+https, so pip
# doesn't run `git submodule update --init --recursive` — that pulls
# evaluation/easi/* (hundreds of MB of benchmarking subrepos) which ComfyUI
# users never need. GitHub's archive tarball already strips submodule trees.
# Monorepo developers can `pip install -e .` from the repo root instead and
# skip this line via `pip install -r requirements.txt --no-deps`.
sensenova-u1 @ https://github.com/OpenSenseNova/SenseNova-U1/archive/refs/heads/main.tar.gz
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
// SenseNova Interleave Preview renders text and images in their original
// interleaved order on the node. The backend pushes a structured
// `ui.parts` array; we map each entry to a DOM node here.
const STYLE_ID = "sensenova-interleave-preview-styles";
const STYLE_CSS = `
.sn-interleave {
padding: 8px;
box-sizing: border-box;
overflow: auto;
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
font-size: 13px;
line-height: 1.5;
color: var(--input-text, #ddd);
background: var(--comfy-input-bg, #1e1e1e);
border: 1px solid var(--border-color, #333);
border-radius: 6px;
word-break: break-word;
}
.sn-interleave > * { margin: 0 0 8px 0; }
.sn-interleave-text { white-space: pre-wrap; }
.sn-interleave-think {
padding: 6px 8px;
border-left: 3px solid var(--node-selected-color, #6c757d);
background: var(--comfy-menu-bg, #2a2a2a);
color: var(--descrip-text, #aaa);
font-style: italic;
white-space: pre-wrap;
}
.sn-interleave-think summary {
cursor: pointer;
font-style: normal;
font-weight: 600;
}
.sn-interleave-think > div { margin-top: 4px; }
.sn-interleave-image { text-align: center; }
.sn-interleave-image img {
max-width: 100%;
max-height: 480px;
border-radius: 4px;
border: 1px solid var(--border-color, #333);
}
.sn-interleave-placeholder {
color: var(--descrip-text, #888);
font-style: italic;
}
`;
function ensureStyles() {
if (document.getElementById(STYLE_ID)) return;
const style = document.createElement("style");
style.id = STYLE_ID;
style.textContent = STYLE_CSS;
document.head.appendChild(style);
}
function buildImageUrl(part) {
const params = new URLSearchParams({
filename: part.filename || "",
type: part.image_type || "temp",
subfolder: part.subfolder || "",
// Cache-bust because temp filenames may collide across runs.
rand: Math.random().toString(36).slice(2),
});
return api?.apiURL ? api.apiURL(`/view?${params}`) : `/view?${params}`;
}
const RENDERERS = {
text(part) {
const div = document.createElement("div");
div.className = "sn-interleave-text";
div.textContent = part.text || "";
return div;
},
think(part) {
const details = document.createElement("details");
details.className = "sn-interleave-think";
const summary = document.createElement("summary");
summary.textContent = "think";
details.appendChild(summary);
const body = document.createElement("div");
body.textContent = part.text || "";
details.appendChild(body);
return details;
},
image(part) {
const wrap = document.createElement("div");
wrap.className = "sn-interleave-image";
if (part.missing || !part.filename) {
const span = document.createElement("span");
span.className = "sn-interleave-placeholder";
span.textContent = `[image:${part.index} missing]`;
wrap.appendChild(span);
} else {
const img = document.createElement("img");
img.alt = `image ${part.index}`;
img.src = buildImageUrl(part);
wrap.appendChild(img);
}
return wrap;
},
};
function renderParts(container, parts) {
container.innerHTML = "";
if (!parts?.length) {
const empty = document.createElement("div");
empty.className = "sn-interleave-placeholder";
empty.textContent = "(no interleaved output)";
container.appendChild(empty);
return;
}
for (const part of parts) {
const renderer = RENDERERS[part.type];
if (renderer) container.appendChild(renderer(part));
}
}
app.registerExtension({
name: "sensenova.interleave_preview",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData?.name !== "SenseNovaInterleavePreview") return;
ensureStyles();
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const result = onNodeCreated?.apply(this, arguments);
const container = document.createElement("div");
container.className = "sn-interleave";
const hint = document.createElement("div");
hint.className = "sn-interleave-placeholder";
hint.textContent = "Interleave preview output will appear here after the workflow runs.";
container.appendChild(hint);
this.addDOMWidget?.("preview", "interleave_preview", container, {
serialize: false,
hideOnZoom: false,
});
this._snContainer = container;
// Suppress ComfyUI's default node-header image strip; we render images inline.
this.imgs = [];
return result;
};
const onExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function (message) {
onExecuted?.apply(this, arguments);
if (!this._snContainer) return;
renderParts(this._snContainer, Array.isArray(message?.parts) ? message.parts : []);
this.imgs = [];
this.setDirtyCanvas?.(true, true);
};
},
});
Place demo case images referenced by `README.md` / `README_CN.md` in this folder.
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment