Release 0.1.3: isolate Prompter and migrate saved workflows
Browse files- CHANGELOG.md +7 -0
- DMAI-NODES-v0.1.3.zip +3 -0
- DMAI-NODES-v0.1.3.zip.sha256 +1 -0
- README.md +3 -1
- dmai_nodes/__init__.py +1 -1
- dmai_nodes/nodes.py +3 -3
- docs/API.md +12 -0
- docs/INSTALLATION.md +20 -0
- docs/VALIDATION.md +15 -4
- pyproject.toml +1 -1
- release.json +2 -2
- tools/build_release.py +3 -3
- tools/migrate_workflow.py +196 -0
- tools/run_workflow.py +10 -1
- web/dmai-nodes.js +3 -1
- web/inputs.mjs +2 -2
- web/nodes.css +5 -5
- web/progress.mjs +3 -1
- web/workflow-migration.mjs +76 -0
- workflows/DMAI-NODES-FLUX.api.json +1 -1
- workflows/DMAI-NODES-FLUX.json +2 -2
- workflows/DMAI-NODES-Krea.api.json +1 -1
- workflows/DMAI-NODES-Krea.json +2 -2
- workflows/DMAI-NODES-Qwen.api.json +1 -1
- workflows/DMAI-NODES-Qwen.json +2 -2
- workflows/DMAI-NODES-SDXL.api.json +1 -1
- workflows/DMAI-NODES-SDXL.json +2 -2
CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
| 1 |
# Changelog
|
| 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
## 0.1.2 — 2026-09-28
|
| 4 |
|
| 5 |
- Fixed native socket dragging: DOM panels leave real canvas gutters around input and output hit targets. Hidden JSON widgets no longer affect socket spacing.
|
|
|
|
| 1 |
# Changelog
|
| 2 |
|
| 3 |
+
## 0.1.3 — 2026-09-29
|
| 4 |
+
|
| 5 |
+
- Give the new Prompter the unique internal ID `DMAINodesPrompter`, keeping its visible name **DMAI Prompter**. Older DMAI Suite and standalone Prompter installations can keep their original node and frontend hooks.
|
| 6 |
+
- Upgrade identifiable DMAI NODES 0.1.0-0.1.2 UI workflows before loading; preserve legacy Prompters, node IDs, links and settings.
|
| 7 |
+
- Update starter workflows and progress tracking. Add an offline workflow migration tool and migration in the API submission helper, preserving source files.
|
| 8 |
+
- Isolate the new LoRA row and filename CSS classes from the older DMAI LoRA Loader styles.
|
| 9 |
+
|
| 10 |
## 0.1.2 — 2026-09-28
|
| 11 |
|
| 12 |
- Fixed native socket dragging: DOM panels leave real canvas gutters around input and output hit targets. Hidden JSON widgets no longer affect socket spacing.
|
DMAI-NODES-v0.1.3.zip
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1a68ce0492afcfcfd16894affb05b1667661fd9658b79be4d590139771ab07d2
|
| 3 |
+
size 149760
|
DMAI-NODES-v0.1.3.zip.sha256
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
1a68ce0492afcfcfd16894affb05b1667661fd9658b79be4d590139771ab07d2 DMAI-NODES-v0.1.3.zip
|
README.md
CHANGED
|
@@ -14,7 +14,7 @@ tags:
|
|
| 14 |
|
| 15 |
**A compact image workflow for ComfyUI.** Slate surfaces, clean Inter typography and English controls.
|
| 16 |
|
| 17 |
-
**0.1.
|
| 18 |
|
| 19 |
Prompter → Generation Engine → Gallery, with an ordered LoRA Loader feeding the engine.
|
| 20 |
|
|
@@ -44,6 +44,8 @@ python -m pip install -r custom_nodes/DMAI-NODES/requirements.txt
|
|
| 44 |
|
| 45 |
Restart ComfyUI and refresh the browser. Search for **DMAI** in the node library.
|
| 46 |
|
|
|
|
|
|
|
| 47 |
- [Windows Portable, local environments and Runpod](docs/INSTALLATION.md)
|
| 48 |
- [Model files, presets and enhancement](docs/MODELS.md)
|
| 49 |
- [API workflow and future website integration](docs/API.md)
|
|
|
|
| 14 |
|
| 15 |
**A compact image workflow for ComfyUI.** Slate surfaces, clean Inter typography and English controls.
|
| 16 |
|
| 17 |
+
**0.1.3 preview** · [GitHub releases](https://github.com/ceroomedia/DMAI-NODES/releases) · [Hugging Face download mirror](https://huggingface.co/ceroomedia/DMAI-NODES)
|
| 18 |
|
| 19 |
Prompter → Generation Engine → Gallery, with an ordered LoRA Loader feeding the engine.
|
| 20 |
|
|
|
|
| 44 |
|
| 45 |
Restart ComfyUI and refresh the browser. Search for **DMAI** in the node library.
|
| 46 |
|
| 47 |
+
**Using an older DMAI package too?** Version 0.1.3 gives this package's Prompter its own internal ID, `DMAINodesPrompter`. Choose **DMAI Prompter** from the **DMAI-NODES** package in category **DMAI NODES**. The older Suite and standalone Prompter keep their existing IDs. See [updating from 0.1.2](docs/INSTALLATION.md#updating-from-012-or-earlier) for saved workflows and API clients.
|
| 48 |
+
|
| 49 |
- [Windows Portable, local environments and Runpod](docs/INSTALLATION.md)
|
| 50 |
- [Model files, presets and enhancement](docs/MODELS.md)
|
| 51 |
- [API workflow and future website integration](docs/API.md)
|
dmai_nodes/__init__.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
| 1 |
"""DMAI NODES domain modules; importing these does not initialize a GPU."""
|
| 2 |
-
__version__ = "0.1.
|
|
|
|
| 1 |
"""DMAI NODES domain modules; importing these does not initialize a GPU."""
|
| 2 |
+
__version__ = "0.1.3"
|
dmai_nodes/nodes.py
CHANGED
|
@@ -22,10 +22,10 @@ def json_config(value):
|
|
| 22 |
return read_json(value)
|
| 23 |
|
| 24 |
|
| 25 |
-
class
|
| 26 |
@classmethod
|
| 27 |
def define_schema(cls):
|
| 28 |
-
return io.Schema(node_id="
|
| 29 |
description="Prompt, dimensions and image count. Optional text input is prepended to local text.",
|
| 30 |
inputs=[io.String.Input("config_json",default=json.dumps(DEFAULT_PROMPT),multiline=True,socketless=True),
|
| 31 |
io.String.Input("text",optional=True,force_input=True,advanced=True,tooltip="Optional source text, followed by the local prompt.")],
|
|
@@ -139,4 +139,4 @@ class DMAIVAEDecode(io.ComfyNode):
|
|
| 139 |
return io.NodeOutput(VAEDecode().decode(vae,latent)[0])
|
| 140 |
|
| 141 |
|
| 142 |
-
NODE_CLASSES=[
|
|
|
|
| 22 |
return read_json(value)
|
| 23 |
|
| 24 |
|
| 25 |
+
class DMAINodesPrompter(io.ComfyNode):
|
| 26 |
@classmethod
|
| 27 |
def define_schema(cls):
|
| 28 |
+
return io.Schema(node_id="DMAINodesPrompter",display_name="DMAI Prompter",category="DMAI NODES",
|
| 29 |
description="Prompt, dimensions and image count. Optional text input is prepended to local text.",
|
| 30 |
inputs=[io.String.Input("config_json",default=json.dumps(DEFAULT_PROMPT),multiline=True,socketless=True),
|
| 31 |
io.String.Input("text",optional=True,force_input=True,advanced=True,tooltip="Optional source text, followed by the local prompt.")],
|
|
|
|
| 139 |
return io.NodeOutput(VAEDecode().decode(vae,latent)[0])
|
| 140 |
|
| 141 |
|
| 142 |
+
NODE_CLASSES=[DMAINodesPrompter,DMAILoRAStack,DMAIGenerationEngine,DMAIGallery,DMAIKreaEnhancer,DMAIVAEEncode,DMAIVAEDecode]
|
docs/API.md
CHANGED
|
@@ -24,6 +24,18 @@ In a completed starter job, Gallery metadata is at `history[prompt_id]["outputs"
|
|
| 24 |
|
| 25 |
Use seed strings for the full unsigned 64-bit range; JavaScript numbers cannot represent all of it exactly. Each image's exact seed is recorded in the Engine report. The CLI timeout only stops waiting; it does not cancel the job.
|
| 26 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
## Generation progress over WebSocket
|
| 28 |
|
| 29 |
Connect to ComfyUI's `/ws?clientId=<client ID>` before submitting the workflow. Use the same ID as `client_id` in `POST /prompt`. The Engine sends `dmai_generation_progress` messages to that client through ComfyUI's existing WebSocket:
|
|
|
|
| 24 |
|
| 25 |
Use seed strings for the full unsigned 64-bit range; JavaScript numbers cannot represent all of it exactly. Each image's exact seed is recorded in the Engine report. The CLI timeout only stops waiting; it does not cancel the job.
|
| 26 |
|
| 27 |
+
## Prompter ID migration
|
| 28 |
+
|
| 29 |
+
Version 0.1.3 uses `DMAINodesPrompter` as the Prompter's API `class_type`. The request configuration, `DMAI_PROMPT` output and Engine connections keep the same contracts. Older DMAI packages still own `DMAIPrompter`; the server intentionally has no alias that could overwrite them.
|
| 30 |
+
|
| 31 |
+
Use the updated starter API files for new integrations. The included `run_workflow.py` converts identifiable 0.1.0-0.1.2 DMAI NODES Prompters in memory before submitting and leaves the source file intact. Direct `/prompt` callers should migrate their template first:
|
| 32 |
+
|
| 33 |
+
```bash
|
| 34 |
+
python custom_nodes/DMAI-NODES/tools/migrate_workflow.py old-workflow.api.json --output migrated-workflow.api.json
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
The offline tool also accepts saved UI workflows. It requires a new output path and never replaces the source. Only records matching the new package's configuration and connection contracts are converted. Legacy CLIP/conditioning Prompters and ambiguous records remain unchanged; do not replace every occurrence of `DMAIPrompter` globally. UI workflows loaded in Comfy's frontend are migrated before nodes are created.
|
| 38 |
+
|
| 39 |
## Generation progress over WebSocket
|
| 40 |
|
| 41 |
Connect to ComfyUI's `/ws?clientId=<client ID>` before submitting the workflow. Use the same ID as `client_id` in `POST /prompt`. The Engine sends `dmai_generation_progress` messages to that client through ComfyUI's existing WebSocket:
|
docs/INSTALLATION.md
CHANGED
|
@@ -77,9 +77,29 @@ git -C .\ComfyUI\custom_nodes\DMAI-NODES pull --ff-only
|
|
| 77 |
|
| 78 |
Restart ComfyUI afterward. For ZIP installations, replace package files from the new release while preserving your workflows and output. To uninstall, remove only the custom-node folder after stopping ComfyUI. Saved originals and the gallery index are in `output/DMAI-NODES` and are not removed by package updates.
|
| 79 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
## Troubleshooting
|
| 81 |
|
| 82 |
- **No DMAI nodes:** inspect the ComfyUI console for import errors; check the Python environment and one-level ZIP extraction.
|
|
|
|
| 83 |
- **Missing model:** select the exact catalog entry, including subfolder. Refresh the model list after adding files.
|
| 84 |
- **Enhancer unavailable:** install the pinned dependency or choose Clean Base/Manual with `none`.
|
| 85 |
- **LoRA has no matching patches:** verify the LoRA family and selected base model; this is deliberately an error.
|
|
|
|
| 77 |
|
| 78 |
Restart ComfyUI afterward. For ZIP installations, replace package files from the new release while preserving your workflows and output. To uninstall, remove only the custom-node folder after stopping ComfyUI. Saved originals and the gallery index are in `output/DMAI-NODES` and are not removed by package updates.
|
| 79 |
|
| 80 |
+
### Updating from 0.1.2 or earlier
|
| 81 |
+
|
| 82 |
+
The new Prompter now registers as `DMAINodesPrompter`. Its visible name remains **DMAI Prompter**, under **DMAI NODES**, with the **DMAI-NODES** package badge. Older DMAI Suite and standalone Prompter installations use `DMAIPrompter`; they can remain enabled alongside this package.
|
| 83 |
+
|
| 84 |
+
To install exactly this release from an existing Git checkout, including a checkout pinned to an older tag, run inside `custom_nodes/DMAI-NODES`:
|
| 85 |
+
|
| 86 |
+
```bash
|
| 87 |
+
git fetch origin tag v0.1.3
|
| 88 |
+
git checkout v0.1.3
|
| 89 |
+
python -m pip install -r requirements.txt
|
| 90 |
+
git describe --tags --exact-match
|
| 91 |
+
```
|
| 92 |
+
|
| 93 |
+
Use ComfyUI's Python environment for the dependency command; Windows Portable uses `..\..\..\python_embeded\python.exe` from that node directory. Restart ComfyUI, then hard-refresh the browser with Ctrl+F5.
|
| 94 |
+
|
| 95 |
+
UI workflows saved by DMAI NODES 0.1.0-0.1.2 are upgraded on load only when their Prompter data and output identify this package. Save the workflow after loading it. Legacy Suite/standalone Prompters keep their original IDs and connections. Ambiguous or damaged records are left for manual correction.
|
| 96 |
+
|
| 97 |
+
For API JSON sent directly to `/prompt`, use the new `DMAINodesPrompter` class type. The included API helper and offline conversion tool handle identifiable older DMAI NODES API workflows; see [API migration](API.md#prompter-id-migration). The server does not register the conflicting old ID as an alias.
|
| 98 |
+
|
| 99 |
## Troubleshooting
|
| 100 |
|
| 101 |
- **No DMAI nodes:** inspect the ComfyUI console for import errors; check the Python environment and one-level ZIP extraction.
|
| 102 |
+
- **Prompter missing when older DMAI nodes are installed:** update to 0.1.3 or newer, restart ComfyUI and hard-refresh. Search `DMAINodesPrompter` or select **DMAI Prompter** with the **DMAI-NODES** badge. The new node has a `request` output of type `DMAI_PROMPT`.
|
| 103 |
- **Missing model:** select the exact catalog entry, including subfolder. Refresh the model list after adding files.
|
| 104 |
- **Enhancer unavailable:** install the pinned dependency or choose Clean Base/Manual with `none`.
|
| 105 |
- **LoRA has no matching patches:** verify the LoRA family and selected base model; this is deliberately an error.
|
docs/VALIDATION.md
CHANGED
|
@@ -1,6 +1,17 @@
|
|
| 1 |
-
# Validation — 0.1.
|
| 2 |
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
## Reference environment
|
| 6 |
|
|
@@ -14,8 +25,8 @@ Checked on 2026-09-28. This release has working ComfyUI registration, CPU contra
|
|
| 14 |
|
| 15 |
| Check | Evidence |
|
| 16 |
| --- | --- |
|
| 17 |
-
| Python suite |
|
| 18 |
-
| JavaScript suite |
|
| 19 |
| Node registration | All seven native V3 nodes appear in real `/object_info`. |
|
| 20 |
| Runtime choices | Four model profiles, 45 native sampler names and 9 schedulers in the test installation. Extension installations can change these counts. |
|
| 21 |
| LoRA loading | Four real CPU comparisons use tiny safetensors, Comfy's file loader, ModelPatcher and LoRA weight calculation. Results match the standard loader and expected low-rank math exactly. |
|
|
|
|
| 1 |
+
# Validation — 0.1.3 preview
|
| 2 |
|
| 3 |
+
Updated on 2026-09-29. This release has working ComfyUI registration, CPU contract tests and real local API checks. GPU image generation is still pending. The coexistence checks below were added for 0.1.3; earlier Gallery, model-picker and connector observations were made on 2026-09-28.
|
| 4 |
+
|
| 5 |
+
## 0.1.3 coexistence and migration
|
| 6 |
+
|
| 7 |
+
- Real ComfyUI loader tests load the new extension alongside legacy V1 and Suite-style V3 Prompter fixtures in both orders. Each ID retains its own schema and execution contract. The new extension does not claim `DMAIPrompter`.
|
| 8 |
+
- A running CPU ComfyUI instance loaded the actual older **ComfyUI-DMAI-Suite** and the new package together. `/object_info` contained all seven new nodes and all four Suite nodes. `DMAINodesPrompter` returned `DMAI_PROMPT`; legacy `DMAIPrompter` retained `CONDITIONING, INT, INT, STRING`.
|
| 9 |
+
- In the real Canvas frontend, both Prompters appeared in their separate package categories and rendered their own controls. A saved 0.1.2 DMAI NODES workflow upgraded on load with its five links, 1152x768 dimensions, image count 2, model choices and LoRA strengths intact. A legacy Prompter was added, edited, saved and reloaded alongside it; its original ID and widget format were preserved.
|
| 10 |
+
- JavaScript regressions cover nested serialized subgraphs, named/positional widget formats, invalid or ambiguous signatures, unchanged settings/links and canonical progress tracking. The new frontend mounts only `DMAINodesPrompter`.
|
| 11 |
+
- Eleven Python migration tests cover mixed UI/API graphs, conservative signature checks, invalid links, UTF-8/configuration boundaries, source/output preservation and rejection of legacy nodes before API submission.
|
| 12 |
+
- The new LoRA row/name classes are isolated from the matching selectors found in older DMAI frontend styles.
|
| 13 |
+
|
| 14 |
+
These checks establish registration, UI coexistence and workflow migration. They do not add GPU inference coverage or certify unrelated third-party extension combinations.
|
| 15 |
|
| 16 |
## Reference environment
|
| 17 |
|
|
|
|
| 25 |
|
| 26 |
| Check | Evidence |
|
| 27 |
| --- | --- |
|
| 28 |
+
| Python suite | 99 tests passed with optional real-Comfy CPU tests enabled, including registration coexistence, workflow migration, scoped native progress registry updates and multi-image progress. |
|
| 29 |
+
| JavaScript suite | 64 tests passed: dimensions, exact seeds, ordered manifests, strength edits, mode persistence, sampler overrides, ZIP selection, keyboard containment, Comfy change events, progress presentation, job lifecycle handling, native socket geometry, model-file selection and safe workflow migration. |
|
| 30 |
| Node registration | All seven native V3 nodes appear in real `/object_info`. |
|
| 31 |
| Runtime choices | Four model profiles, 45 native sampler names and 9 schedulers in the test installation. Extension installations can change these counts. |
|
| 32 |
| LoRA loading | Four real CPU comparisons use tiny safetensors, Comfy's file loader, ModelPatcher and LoRA weight calculation. Results match the standard loader and expected low-rank math exactly. |
|
pyproject.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
[project]
|
| 2 |
name = "dmai-nodes"
|
| 3 |
-
version = "0.1.
|
| 4 |
description = "Compact image workflows for ComfyUI: Prompter, ordered LoRAs, Generation Engine and Gallery."
|
| 5 |
authors = [{name = "DMAI / ceroomedia"}]
|
| 6 |
readme = "README.md"
|
|
|
|
| 1 |
[project]
|
| 2 |
name = "dmai-nodes"
|
| 3 |
+
version = "0.1.3"
|
| 4 |
description = "Compact image workflows for ComfyUI: Prompter, ordered LoRAs, Generation Engine and Gallery."
|
| 5 |
authors = [{name = "DMAI / ceroomedia"}]
|
| 6 |
readme = "README.md"
|
release.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
| 1 |
{
|
| 2 |
"name": "DMAI NODES",
|
| 3 |
-
"version": "0.1.
|
| 4 |
"channel": "preview",
|
| 5 |
"license": "MIT",
|
| 6 |
"comfyui_reference": "0.37.0",
|
| 7 |
"frontend_reference": "1.52.7",
|
| 8 |
-
"nodes": ["
|
| 9 |
"model_profiles": ["krea2-turbo", "sdxl-checkpoint", "qwen-image", "flux1-dev"],
|
| 10 |
"contains_model_weights": false,
|
| 11 |
"gpu_inference_verified": false
|
|
|
|
| 1 |
{
|
| 2 |
"name": "DMAI NODES",
|
| 3 |
+
"version": "0.1.3",
|
| 4 |
"channel": "preview",
|
| 5 |
"license": "MIT",
|
| 6 |
"comfyui_reference": "0.37.0",
|
| 7 |
"frontend_reference": "1.52.7",
|
| 8 |
+
"nodes": ["DMAINodesPrompter", "DMAILoRAStack", "DMAIGenerationEngine", "DMAIGallery", "DMAIKreaEnhancer", "DMAIVAEEncode", "DMAIVAEDecode"],
|
| 9 |
"model_profiles": ["krea2-turbo", "sdxl-checkpoint", "qwen-image", "flux1-dev"],
|
| 10 |
"contains_model_weights": false,
|
| 11 |
"gpu_inference_verified": false
|
tools/build_release.py
CHANGED
|
@@ -7,7 +7,7 @@ import shutil
|
|
| 7 |
import zipfile
|
| 8 |
|
| 9 |
ROOT=Path(__file__).resolve().parents[1]
|
| 10 |
-
VERSION="0.1.
|
| 11 |
ROOT_FILES={"__init__.py","README.md","LICENSE","pyproject.toml","requirements.txt","THIRD_PARTY_NOTICES.md","CHANGELOG.md","release.json"}
|
| 12 |
DIRECTORIES={"dmai_nodes","web","docs","resources","licenses","workflows"}
|
| 13 |
FORBIDDEN={".git","__pycache__",".validation","node_modules","dist"}
|
|
@@ -23,7 +23,7 @@ def included_files():
|
|
| 23 |
if path.is_symlink():continue
|
| 24 |
parts=path.relative_to(ROOT).parts
|
| 25 |
if (len(parts)==1 and parts[0] in ROOT_FILES) or (parts[0] in DIRECTORIES and path.suffix in EXTENSIONS):result.append(path)
|
| 26 |
-
elif len(parts)==2 and parts[0]=="tools" and path.name in {"run_workflow.py","build_release.py"}:result.append(path)
|
| 27 |
return sorted(result,key=lambda item:item.relative_to(ROOT).as_posix())
|
| 28 |
|
| 29 |
|
|
@@ -38,7 +38,7 @@ def build():
|
|
| 38 |
path=destination/f"DMAI-NODES-v{VERSION}.zip"
|
| 39 |
files=included_files()
|
| 40 |
names={item.relative_to(ROOT).as_posix() for item in files}
|
| 41 |
-
required={"__init__.py","README.md","LICENSE","dmai_nodes/nodes.py","dmai_nodes/engine.py","dmai_nodes/lora.py","dmai_nodes/gallery.py","dmai_nodes/routes.py","web/dmai-nodes.js","requirements.txt","workflows/DMAI-NODES-Krea.json"}
|
| 42 |
missing=required-names
|
| 43 |
if missing:raise RuntimeError(f"Missing release files: {missing}")
|
| 44 |
with zipfile.ZipFile(path,"w",zipfile.ZIP_DEFLATED,compresslevel=9) as archive:
|
|
|
|
| 7 |
import zipfile
|
| 8 |
|
| 9 |
ROOT=Path(__file__).resolve().parents[1]
|
| 10 |
+
VERSION="0.1.3"
|
| 11 |
ROOT_FILES={"__init__.py","README.md","LICENSE","pyproject.toml","requirements.txt","THIRD_PARTY_NOTICES.md","CHANGELOG.md","release.json"}
|
| 12 |
DIRECTORIES={"dmai_nodes","web","docs","resources","licenses","workflows"}
|
| 13 |
FORBIDDEN={".git","__pycache__",".validation","node_modules","dist"}
|
|
|
|
| 23 |
if path.is_symlink():continue
|
| 24 |
parts=path.relative_to(ROOT).parts
|
| 25 |
if (len(parts)==1 and parts[0] in ROOT_FILES) or (parts[0] in DIRECTORIES and path.suffix in EXTENSIONS):result.append(path)
|
| 26 |
+
elif len(parts)==2 and parts[0]=="tools" and path.name in {"run_workflow.py","migrate_workflow.py","build_release.py"}:result.append(path)
|
| 27 |
return sorted(result,key=lambda item:item.relative_to(ROOT).as_posix())
|
| 28 |
|
| 29 |
|
|
|
|
| 38 |
path=destination/f"DMAI-NODES-v{VERSION}.zip"
|
| 39 |
files=included_files()
|
| 40 |
names={item.relative_to(ROOT).as_posix() for item in files}
|
| 41 |
+
required={"__init__.py","README.md","LICENSE","dmai_nodes/nodes.py","dmai_nodes/engine.py","dmai_nodes/lora.py","dmai_nodes/gallery.py","dmai_nodes/routes.py","web/dmai-nodes.js","web/workflow-migration.mjs","tools/migrate_workflow.py","requirements.txt","workflows/DMAI-NODES-Krea.json"}
|
| 42 |
missing=required-names
|
| 43 |
if missing:raise RuntimeError(f"Missing release files: {missing}")
|
| 44 |
with zipfile.ZipFile(path,"w",zipfile.ZIP_DEFLATED,compresslevel=9) as archive:
|
tools/migrate_workflow.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Copy an old DMAI NODES workflow to the conflict-free Prompter ID.
|
| 2 |
+
|
| 3 |
+
The old Suite's CLIP Prompter and ambiguous nodes are left untouched. This tool
|
| 4 |
+
uses only the Python standard library and never overwrites the input or output.
|
| 5 |
+
"""
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
import argparse
|
| 9 |
+
from copy import deepcopy
|
| 10 |
+
import json
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
OLD_PROMPTER_ID = "DMAIPrompter"
|
| 15 |
+
PROMPTER_ID = "DMAINodesPrompter"
|
| 16 |
+
PROMPT_KEYS = {"schema_version", "prompt", "negative_prompt", "width", "height", "count"}
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def _unique_object(pairs):
|
| 20 |
+
result = {}
|
| 21 |
+
for key, value in pairs:
|
| 22 |
+
if key in result:
|
| 23 |
+
raise ValueError(f"Duplicate JSON key: {key}")
|
| 24 |
+
result[key] = value
|
| 25 |
+
return result
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def _invalid_constant(value):
|
| 29 |
+
raise ValueError(f"Invalid JSON constant: {value}")
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def read_json(raw):
|
| 33 |
+
return json.loads(raw, object_pairs_hook=_unique_object, parse_constant=_invalid_constant)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def _prompt_config(raw):
|
| 37 |
+
"""Require the complete v1 request contract, not a coincidental JSON field."""
|
| 38 |
+
if not isinstance(raw, str) or len(raw.encode("utf-8")) > 128 * 1024:
|
| 39 |
+
return None
|
| 40 |
+
try:
|
| 41 |
+
value = read_json(raw)
|
| 42 |
+
except (ValueError, TypeError):
|
| 43 |
+
return None
|
| 44 |
+
if not isinstance(value, dict) or set(value) != PROMPT_KEYS:
|
| 45 |
+
return None
|
| 46 |
+
if type(value["schema_version"]) is not int or value["schema_version"] != 1:
|
| 47 |
+
return None
|
| 48 |
+
for key in ("prompt", "negative_prompt"):
|
| 49 |
+
if not isinstance(value[key], str) or len(value[key]) > 65536 or "\x00" in value[key]:
|
| 50 |
+
return None
|
| 51 |
+
for key in ("width", "height"):
|
| 52 |
+
if type(value[key]) is not int or not 64 <= value[key] <= 8192 or value[key] % 64:
|
| 53 |
+
return None
|
| 54 |
+
if value["width"] * value["height"] > 16777216:
|
| 55 |
+
return None
|
| 56 |
+
if type(value["count"]) is not int or not 1 <= value["count"] <= 8:
|
| 57 |
+
return None
|
| 58 |
+
return value
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def _is_ui_prompter(node):
|
| 62 |
+
outputs = node.get("outputs")
|
| 63 |
+
if not isinstance(outputs, list) or len(outputs) != 1:
|
| 64 |
+
return False
|
| 65 |
+
output = outputs[0]
|
| 66 |
+
if (not isinstance(output, dict) or output.get("name") != "request"
|
| 67 |
+
or output.get("type") != "DMAI_PROMPT"
|
| 68 |
+
or ("slot_index" in output and (type(output["slot_index"]) is not int or output["slot_index"] != 0))):
|
| 69 |
+
return False
|
| 70 |
+
inputs = node.get("inputs", [])
|
| 71 |
+
if not isinstance(inputs, list) or any(
|
| 72 |
+
not isinstance(item, dict) or item.get("name") not in {"config_json", "text"}
|
| 73 |
+
or item.get("type") != "STRING" for item in inputs
|
| 74 |
+
):
|
| 75 |
+
return False
|
| 76 |
+
if len({item["name"] for item in inputs}) != len(inputs):
|
| 77 |
+
return False
|
| 78 |
+
candidates = []
|
| 79 |
+
if "widgets_values" in node:
|
| 80 |
+
values = node["widgets_values"]
|
| 81 |
+
if not isinstance(values, list) or len(values) != 1:
|
| 82 |
+
return False
|
| 83 |
+
candidates.append(_prompt_config(values[0]))
|
| 84 |
+
if "widgets_values_named" in node:
|
| 85 |
+
values = node["widgets_values_named"]
|
| 86 |
+
if not isinstance(values, dict) or set(values) != {"config_json"}:
|
| 87 |
+
return False
|
| 88 |
+
candidates.append(_prompt_config(values["config_json"]))
|
| 89 |
+
return bool(candidates) and all(value is not None and value == candidates[0] for value in candidates)
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def _link_from(value, node_id):
|
| 93 |
+
return (isinstance(value, list) and len(value) == 2
|
| 94 |
+
and type(value[0]) in (str, int) and str(value[0]) == str(node_id))
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def _is_api_prompter(node_id, node, graph):
|
| 98 |
+
inputs = node.get("inputs")
|
| 99 |
+
if (not isinstance(inputs, dict) or set(inputs) - {"config_json", "text"}
|
| 100 |
+
or _prompt_config(inputs.get("config_json")) is None):
|
| 101 |
+
return False
|
| 102 |
+
# API JSON has no typed outputs. A slot-zero request link to our Engine is
|
| 103 |
+
# the extra evidence that distinguishes this from a legacy Suite node.
|
| 104 |
+
linked_engine = False
|
| 105 |
+
for target in graph.values():
|
| 106 |
+
if not isinstance(target, dict) or not isinstance(target.get("inputs"), dict):
|
| 107 |
+
continue
|
| 108 |
+
for name, value in target["inputs"].items():
|
| 109 |
+
if not _link_from(value, node_id):
|
| 110 |
+
continue
|
| 111 |
+
if type(value[1]) is not int or value[1] != 0:
|
| 112 |
+
return False
|
| 113 |
+
if target.get("class_type") == "DMAIGenerationEngine" and name == "request":
|
| 114 |
+
linked_engine = True
|
| 115 |
+
return linked_engine
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def migrate_workflow(document):
|
| 119 |
+
"""Return a migrated deep copy plus node paths; never change the caller's data."""
|
| 120 |
+
if not isinstance(document, dict):
|
| 121 |
+
raise ValueError("Expected a ComfyUI workflow or API prompt JSON object.")
|
| 122 |
+
result = deepcopy(document)
|
| 123 |
+
report = {"migrated": [], "preserved": []}
|
| 124 |
+
|
| 125 |
+
def visit_graph(graph, path):
|
| 126 |
+
if not isinstance(graph, dict):
|
| 127 |
+
return
|
| 128 |
+
nodes = graph.get("nodes")
|
| 129 |
+
if isinstance(nodes, list):
|
| 130 |
+
for index, node in enumerate(nodes):
|
| 131 |
+
if not isinstance(node, dict) or node.get("type") != OLD_PROMPTER_ID:
|
| 132 |
+
continue
|
| 133 |
+
location = f"{path}.nodes[{index}]"
|
| 134 |
+
if _is_ui_prompter(node):
|
| 135 |
+
node["type"] = PROMPTER_ID
|
| 136 |
+
properties = node.get("properties")
|
| 137 |
+
if isinstance(properties, dict) and properties.get("Node name for S&R") == OLD_PROMPTER_ID:
|
| 138 |
+
properties["Node name for S&R"] = PROMPTER_ID
|
| 139 |
+
report["migrated"].append(location)
|
| 140 |
+
else:
|
| 141 |
+
report["preserved"].append(location)
|
| 142 |
+
else:
|
| 143 |
+
for node_id, node in graph.items():
|
| 144 |
+
if not isinstance(node, dict) or node.get("class_type") != OLD_PROMPTER_ID:
|
| 145 |
+
continue
|
| 146 |
+
location = f"{path}[{node_id!r}]"
|
| 147 |
+
if _is_api_prompter(node_id, node, graph):
|
| 148 |
+
node["class_type"] = PROMPTER_ID
|
| 149 |
+
report["migrated"].append(location)
|
| 150 |
+
else:
|
| 151 |
+
report["preserved"].append(location)
|
| 152 |
+
# Serialized LiteGraph subgraphs live in definitions.subgraphs. Do not
|
| 153 |
+
# recursively scan arbitrary metadata, prompt text, or node properties.
|
| 154 |
+
definitions = graph.get("definitions")
|
| 155 |
+
if isinstance(definitions, dict) and isinstance(definitions.get("subgraphs"), list):
|
| 156 |
+
for index, subgraph in enumerate(definitions["subgraphs"]):
|
| 157 |
+
visit_graph(subgraph, f"{path}.definitions.subgraphs[{index}]")
|
| 158 |
+
for key in ("workflow", "prompt"):
|
| 159 |
+
if isinstance(graph.get(key), dict) and "class_type" not in graph[key]:
|
| 160 |
+
visit_graph(graph[key], f"{path}.{key}")
|
| 161 |
+
extra_data = graph.get("extra_data")
|
| 162 |
+
if isinstance(extra_data, dict):
|
| 163 |
+
extra_pnginfo = extra_data.get("extra_pnginfo")
|
| 164 |
+
if isinstance(extra_pnginfo, dict):
|
| 165 |
+
visit_graph(extra_pnginfo.get("workflow"), f"{path}.extra_data.extra_pnginfo.workflow")
|
| 166 |
+
|
| 167 |
+
visit_graph(result, "$")
|
| 168 |
+
return result, report
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
def main(argv=None):
|
| 172 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 173 |
+
parser.add_argument("input", type=Path, help="Existing UI workflow or API prompt JSON")
|
| 174 |
+
parser.add_argument("--output", required=True, type=Path, help="New JSON filename; must not already exist")
|
| 175 |
+
args = parser.parse_args(argv)
|
| 176 |
+
if args.input.resolve() == args.output.resolve():
|
| 177 |
+
parser.error("Input and output must be different files. The original is always preserved.")
|
| 178 |
+
if args.output.exists():
|
| 179 |
+
parser.error("Output already exists. Choose a new filename; existing files are never overwritten.")
|
| 180 |
+
try:
|
| 181 |
+
document = read_json(args.input.read_text(encoding="utf-8-sig"))
|
| 182 |
+
result, report = migrate_workflow(document)
|
| 183 |
+
data = (json.dumps(result, indent=2, ensure_ascii=False, allow_nan=False) + "\n").encode("utf-8")
|
| 184 |
+
with args.output.open("xb") as output:
|
| 185 |
+
output.write(data)
|
| 186 |
+
except (OSError, ValueError, TypeError) as error:
|
| 187 |
+
parser.error(str(error))
|
| 188 |
+
print(f"Saved: {args.output}")
|
| 189 |
+
print(f"Migrated {len(report['migrated'])} DMAI NODES Prompter node(s).")
|
| 190 |
+
print(f"Preserved {len(report['preserved'])} legacy or ambiguous DMAIPrompter node(s).")
|
| 191 |
+
for path in report["preserved"]:
|
| 192 |
+
print(f" Unchanged: {path}")
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
if __name__ == "__main__":
|
| 196 |
+
main()
|
tools/run_workflow.py
CHANGED
|
@@ -7,6 +7,11 @@ from urllib.error import HTTPError
|
|
| 7 |
from urllib.request import Request,urlopen
|
| 8 |
from uuid import uuid4
|
| 9 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
def call(base,path,data=None):
|
| 12 |
body=None if data is None else json.dumps(data).encode()
|
|
@@ -29,7 +34,11 @@ def main():
|
|
| 29 |
parser.add_argument("--wait",action="store_true")
|
| 30 |
parser.add_argument("--timeout",type=int,default=900)
|
| 31 |
args=parser.parse_args()
|
| 32 |
-
workflow=
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
request=json.loads(workflow["1"]["inputs"]["config_json"])
|
| 34 |
request.update(prompt=args.prompt,count=args.count)
|
| 35 |
workflow["1"]["inputs"]["config_json"]=json.dumps(request)
|
|
|
|
| 7 |
from urllib.request import Request,urlopen
|
| 8 |
from uuid import uuid4
|
| 9 |
|
| 10 |
+
try:
|
| 11 |
+
from .migrate_workflow import PROMPTER_ID, migrate_workflow, read_json
|
| 12 |
+
except ImportError:
|
| 13 |
+
from migrate_workflow import PROMPTER_ID, migrate_workflow, read_json
|
| 14 |
+
|
| 15 |
|
| 16 |
def call(base,path,data=None):
|
| 17 |
body=None if data is None else json.dumps(data).encode()
|
|
|
|
| 34 |
parser.add_argument("--wait",action="store_true")
|
| 35 |
parser.add_argument("--timeout",type=int,default=900)
|
| 36 |
args=parser.parse_args()
|
| 37 |
+
workflow, migration=migrate_workflow(read_json(args.workflow.read_text(encoding="utf-8-sig")))
|
| 38 |
+
if migration["migrated"]:
|
| 39 |
+
print(f"Migrated {len(migration['migrated'])} old DMAI NODES Prompter ID(s) in memory; the file is unchanged.",flush=True)
|
| 40 |
+
if not isinstance(workflow.get("1"),dict) or workflow["1"].get("class_type") != PROMPTER_ID:
|
| 41 |
+
parser.error("This runner requires a DMAI NODES starter API workflow with DMAINodesPrompter at node 1. Legacy Suite or ambiguous Prompters are not converted. Export or use an updated starter API workflow.")
|
| 42 |
request=json.loads(workflow["1"]["inputs"]["config_json"])
|
| 43 |
request.update(prompt=args.prompt,count=args.count)
|
| 44 |
workflow["1"]["inputs"]["config_json"]=json.dumps(request)
|
web/dmai-nodes.js
CHANGED
|
@@ -5,10 +5,11 @@ import { Prompter, LoRAStack } from "./inputs.mjs";
|
|
| 5 |
import { Engine } from "./engine.mjs";
|
| 6 |
import { Gallery } from "./gallery.mjs";
|
| 7 |
import { GenerationProgress } from "./progress.mjs";
|
|
|
|
| 8 |
|
| 9 |
const controllers = new WeakMap();
|
| 10 |
const types = {
|
| 11 |
-
|
| 12 |
DMAILoRAStack: LoRAStack,
|
| 13 |
DMAIGenerationEngine: Engine,
|
| 14 |
DMAIGallery: Gallery,
|
|
@@ -84,6 +85,7 @@ app.registerExtension({
|
|
| 84 |
},
|
| 85 |
nodeCreated: install,
|
| 86 |
loadedGraphNode: install,
|
|
|
|
| 87 |
afterConfigureGraph: refreshGraph,
|
| 88 |
// Every visible socket is the real Comfy socket. No duplicate connection UI.
|
| 89 |
});
|
|
|
|
| 5 |
import { Engine } from "./engine.mjs";
|
| 6 |
import { Gallery } from "./gallery.mjs";
|
| 7 |
import { GenerationProgress } from "./progress.mjs";
|
| 8 |
+
import { PROMPTER_TYPE, migratePreviewPrompters } from "./workflow-migration.mjs";
|
| 9 |
|
| 10 |
const controllers = new WeakMap();
|
| 11 |
const types = {
|
| 12 |
+
[PROMPTER_TYPE]: Prompter,
|
| 13 |
DMAILoRAStack: LoRAStack,
|
| 14 |
DMAIGenerationEngine: Engine,
|
| 15 |
DMAIGallery: Gallery,
|
|
|
|
| 85 |
},
|
| 86 |
nodeCreated: install,
|
| 87 |
loadedGraphNode: install,
|
| 88 |
+
beforeConfigureGraph: migratePreviewPrompters,
|
| 89 |
afterConfigureGraph: refreshGraph,
|
| 90 |
// Every visible socket is the real Comfy socket. No duplicate connection UI.
|
| 91 |
});
|
web/inputs.mjs
CHANGED
|
@@ -351,7 +351,7 @@ export class LoRAStack extends Controller {
|
|
| 351 |
list.setAttribute("aria-label", "Ordered LoRA stack");
|
| 352 |
let dragged = null;
|
| 353 |
this.value.entries.forEach((entry, index) => {
|
| 354 |
-
const row = el("div", `dmai-lora-row${entry.enabled ? "" : " dmai-off"}`);
|
| 355 |
row.dataset.entryId = entry.id;
|
| 356 |
const grip = button(`Reorder ${entry.name}; Alt and arrow keys`, "grip");
|
| 357 |
grip.classList.add("dmai-grip");
|
|
@@ -390,7 +390,7 @@ export class LoRAStack extends Controller {
|
|
| 390 |
dragged = null;
|
| 391 |
});
|
| 392 |
const copy = el("div", "dmai-lora-copy"),
|
| 393 |
-
title = el("span", "dmai-lora-name", entry.name.split(/[\\/]/).pop());
|
| 394 |
title.title = entry.name;
|
| 395 |
copy.append(
|
| 396 |
title,
|
|
|
|
| 351 |
list.setAttribute("aria-label", "Ordered LoRA stack");
|
| 352 |
let dragged = null;
|
| 353 |
this.value.entries.forEach((entry, index) => {
|
| 354 |
+
const row = el("div", `dmai-nodes-lora-row${entry.enabled ? "" : " dmai-off"}`);
|
| 355 |
row.dataset.entryId = entry.id;
|
| 356 |
const grip = button(`Reorder ${entry.name}; Alt and arrow keys`, "grip");
|
| 357 |
grip.classList.add("dmai-grip");
|
|
|
|
| 390 |
dragged = null;
|
| 391 |
});
|
| 392 |
const copy = el("div", "dmai-lora-copy"),
|
| 393 |
+
title = el("span", "dmai-nodes-lora-name", entry.name.split(/[\\/]/).pop());
|
| 394 |
title.title = entry.name;
|
| 395 |
copy.append(
|
| 396 |
title,
|
web/nodes.css
CHANGED
|
@@ -361,7 +361,7 @@
|
|
| 361 |
gap: 3px;
|
| 362 |
margin-bottom: 8px;
|
| 363 |
}
|
| 364 |
-
.dmai-nodes .dmai-lora-row {
|
| 365 |
display: grid;
|
| 366 |
grid-template-columns: 14px minmax(70px, 1fr) 46px 25px 18px 25px;
|
| 367 |
align-items: center;
|
|
@@ -369,13 +369,13 @@
|
|
| 369 |
min-height: 65px;
|
| 370 |
border-bottom: 1px solid var(--dmai-line);
|
| 371 |
}
|
| 372 |
-
.dmai-nodes .dmai-lora-row:last-child {
|
| 373 |
border: 0;
|
| 374 |
}
|
| 375 |
.dmai-nodes .dmai-lora-copy {
|
| 376 |
overflow: hidden;
|
| 377 |
}
|
| 378 |
-
.dmai-nodes .dmai-lora-name {
|
| 379 |
display: block;
|
| 380 |
white-space: nowrap;
|
| 381 |
text-overflow: ellipsis;
|
|
@@ -393,10 +393,10 @@
|
|
| 393 |
padding: 7px 3px;
|
| 394 |
text-align: center;
|
| 395 |
}
|
| 396 |
-
.dmai-nodes .dmai-lora-row > .dmai-icon {
|
| 397 |
width: 25px;
|
| 398 |
}
|
| 399 |
-
.dmai-nodes .dmai-lora-row > .dmai-grip {
|
| 400 |
width: 14px;
|
| 401 |
color: #7d8c81;
|
| 402 |
cursor: grab;
|
|
|
|
| 361 |
gap: 3px;
|
| 362 |
margin-bottom: 8px;
|
| 363 |
}
|
| 364 |
+
.dmai-nodes .dmai-nodes-lora-row {
|
| 365 |
display: grid;
|
| 366 |
grid-template-columns: 14px minmax(70px, 1fr) 46px 25px 18px 25px;
|
| 367 |
align-items: center;
|
|
|
|
| 369 |
min-height: 65px;
|
| 370 |
border-bottom: 1px solid var(--dmai-line);
|
| 371 |
}
|
| 372 |
+
.dmai-nodes .dmai-nodes-lora-row:last-child {
|
| 373 |
border: 0;
|
| 374 |
}
|
| 375 |
.dmai-nodes .dmai-lora-copy {
|
| 376 |
overflow: hidden;
|
| 377 |
}
|
| 378 |
+
.dmai-nodes .dmai-nodes-lora-name {
|
| 379 |
display: block;
|
| 380 |
white-space: nowrap;
|
| 381 |
text-overflow: ellipsis;
|
|
|
|
| 393 |
padding: 7px 3px;
|
| 394 |
text-align: center;
|
| 395 |
}
|
| 396 |
+
.dmai-nodes .dmai-nodes-lora-row > .dmai-icon {
|
| 397 |
width: 25px;
|
| 398 |
}
|
| 399 |
+
.dmai-nodes .dmai-nodes-lora-row > .dmai-grip {
|
| 400 |
width: 14px;
|
| 401 |
color: #7d8c81;
|
| 402 |
cursor: grab;
|
web/progress.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
|
|
|
|
|
| 1 |
// Job identity comes from the submitted API graph, never the current selection.
|
| 2 |
const IDLE = Object.freeze({ phase: "idle", percent: 0 });
|
| 3 |
const ACTIVE = new Set(["queued", "loading", "sampling", "decoding", "saving"]);
|
|
@@ -35,7 +37,7 @@ function connections(entry) {
|
|
| 35 |
const engine = prompt[engineId], link = engine.inputs?.request;
|
| 36 |
if (engine.class_type !== "DMAIGenerationEngine" || !Array.isArray(link)) continue;
|
| 37 |
const sourceId = String(link[0]);
|
| 38 |
-
if (prompt[sourceId]?.class_type !==
|
| 39 |
if (!sources.has(sourceId)) sources.set(sourceId, new Set());
|
| 40 |
sources.get(sourceId).add(engineId);
|
| 41 |
}
|
|
|
|
| 1 |
+
import { PROMPTER_TYPE } from "./workflow-migration.mjs";
|
| 2 |
+
|
| 3 |
// Job identity comes from the submitted API graph, never the current selection.
|
| 4 |
const IDLE = Object.freeze({ phase: "idle", percent: 0 });
|
| 5 |
const ACTIVE = new Set(["queued", "loading", "sampling", "decoding", "saving"]);
|
|
|
|
| 37 |
const engine = prompt[engineId], link = engine.inputs?.request;
|
| 38 |
if (engine.class_type !== "DMAIGenerationEngine" || !Array.isArray(link)) continue;
|
| 39 |
const sourceId = String(link[0]);
|
| 40 |
+
if (prompt[sourceId]?.class_type !== PROMPTER_TYPE) continue;
|
| 41 |
if (!sources.has(sourceId)) sources.set(sourceId, new Set());
|
| 42 |
sources.get(sourceId).add(engineId);
|
| 43 |
}
|
web/workflow-migration.mjs
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { readPrompt } from "./core.mjs";
|
| 2 |
+
|
| 3 |
+
export const PROMPTER_TYPE = "DMAINodesPrompter";
|
| 4 |
+
const PREVIEW_PROMPTER_TYPE = "DMAIPrompter";
|
| 5 |
+
const CONFIG_KEYS = ["schema_version", "prompt", "negative_prompt", "width", "height", "count"];
|
| 6 |
+
const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
| 7 |
+
|
| 8 |
+
function promptConfig(raw) {
|
| 9 |
+
if (typeof raw !== "string" || new TextEncoder().encode(raw).byteLength > 128 * 1024) return null;
|
| 10 |
+
try {
|
| 11 |
+
const value = readPrompt(raw);
|
| 12 |
+
if (
|
| 13 |
+
Object.keys(value).length !== CONFIG_KEYS.length ||
|
| 14 |
+
!CONFIG_KEYS.every((key) => Object.hasOwn(value, key)) ||
|
| 15 |
+
value.width * value.height > 16777216 ||
|
| 16 |
+
[value.prompt, value.negative_prompt].some((text) => [...text].length > 65536 || text.includes("\0"))
|
| 17 |
+
) return null;
|
| 18 |
+
return value;
|
| 19 |
+
} catch { return null; }
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
// The old Suite also owns DMAIPrompter. Its CLIP/conditioning node must never
|
| 23 |
+
// be aliased or rewritten: only the complete 0.1.x request signature is ours.
|
| 24 |
+
export function isPreviewPrompter(node) {
|
| 25 |
+
if (!isObject(node) || node.type !== PREVIEW_PROMPTER_TYPE) return false;
|
| 26 |
+
if (!Array.isArray(node.outputs) || node.outputs.length !== 1) return false;
|
| 27 |
+
const output = node.outputs[0];
|
| 28 |
+
if (
|
| 29 |
+
output?.name !== "request" || output.type !== "DMAI_PROMPT" ||
|
| 30 |
+
(output.slot_index !== undefined && output.slot_index !== 0)
|
| 31 |
+
) return false;
|
| 32 |
+
const inputs = node.inputs ?? [];
|
| 33 |
+
if (!Array.isArray(inputs) || inputs.some((input) =>
|
| 34 |
+
!["text", "config_json"].includes(input?.name) || input.type !== "STRING"
|
| 35 |
+
) || new Set(inputs.map((input) => input.name)).size !== inputs.length) return false;
|
| 36 |
+
|
| 37 |
+
const candidates = [];
|
| 38 |
+
if (node.widgets_values !== undefined) {
|
| 39 |
+
if (!Array.isArray(node.widgets_values) || node.widgets_values.length !== 1) return false;
|
| 40 |
+
candidates.push(node.widgets_values[0]);
|
| 41 |
+
}
|
| 42 |
+
if (node.widgets_values_named !== undefined) {
|
| 43 |
+
const named = node.widgets_values_named;
|
| 44 |
+
if (!isObject(named) || Object.keys(named).length !== 1 || !Object.hasOwn(named, "config_json")) return false;
|
| 45 |
+
candidates.push(named.config_json);
|
| 46 |
+
}
|
| 47 |
+
if (!candidates.length) return false;
|
| 48 |
+
const configs = candidates.map(promptConfig);
|
| 49 |
+
if (configs.some((config) => !config)) return false;
|
| 50 |
+
return configs.every((config) => config && CONFIG_KEYS.every((key) => config[key] === configs[0][key]));
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
// ComfyUI's serialized subgraphs live in definitions.subgraphs, including
|
| 54 |
+
// nested definitions. Runtime node.subgraph and arbitrary metadata are not
|
| 55 |
+
// serialized node collections and must not be rewritten.
|
| 56 |
+
export function migratePreviewPrompters(workflow) {
|
| 57 |
+
const pending = [workflow], visited = new Set();
|
| 58 |
+
let migrated = 0;
|
| 59 |
+
while (pending.length) {
|
| 60 |
+
const graph = pending.pop();
|
| 61 |
+
if (!isObject(graph) || visited.has(graph)) continue;
|
| 62 |
+
visited.add(graph);
|
| 63 |
+
if (Array.isArray(graph.nodes)) {
|
| 64 |
+
for (const node of graph.nodes) {
|
| 65 |
+
if (!isPreviewPrompter(node)) continue;
|
| 66 |
+
node.type = PROMPTER_TYPE;
|
| 67 |
+
if (node.properties?.["Node name for S&R"] === PREVIEW_PROMPTER_TYPE)
|
| 68 |
+
node.properties["Node name for S&R"] = PROMPTER_TYPE;
|
| 69 |
+
migrated++;
|
| 70 |
+
}
|
| 71 |
+
}
|
| 72 |
+
const subgraphs = graph.definitions?.subgraphs;
|
| 73 |
+
if (Array.isArray(subgraphs)) pending.push(...subgraphs);
|
| 74 |
+
}
|
| 75 |
+
return migrated;
|
| 76 |
+
}
|
workflows/DMAI-NODES-FLUX.api.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
{
|
| 2 |
"1": {
|
| 3 |
-
"class_type": "
|
| 4 |
"inputs": {
|
| 5 |
"config_json": "{\"schema_version\": 1, \"prompt\": \"A sculptural object made of dark stone and translucent green glass, soft studio side light.\", \"negative_prompt\": \"\", \"width\": 832, \"height\": 1024, \"count\": 1}"
|
| 6 |
}
|
|
|
|
| 1 |
{
|
| 2 |
"1": {
|
| 3 |
+
"class_type": "DMAINodesPrompter",
|
| 4 |
"inputs": {
|
| 5 |
"config_json": "{\"schema_version\": 1, \"prompt\": \"A sculptural object made of dark stone and translucent green glass, soft studio side light.\", \"negative_prompt\": \"\", \"width\": 832, \"height\": 1024, \"count\": 1}"
|
| 6 |
}
|
workflows/DMAI-NODES-FLUX.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
| 4 |
"nodes": [
|
| 5 |
{
|
| 6 |
"id": 1,
|
| 7 |
-
"type": "
|
| 8 |
"pos": [
|
| 9 |
40,
|
| 10 |
100
|
|
@@ -34,7 +34,7 @@
|
|
| 34 |
}
|
| 35 |
],
|
| 36 |
"properties": {
|
| 37 |
-
"Node name for S&R": "
|
| 38 |
},
|
| 39 |
"widgets_values": [
|
| 40 |
"{\"schema_version\": 1, \"prompt\": \"A sculptural object made of dark stone and translucent green glass, soft studio side light.\", \"negative_prompt\": \"\", \"width\": 832, \"height\": 1024, \"count\": 1}"
|
|
|
|
| 4 |
"nodes": [
|
| 5 |
{
|
| 6 |
"id": 1,
|
| 7 |
+
"type": "DMAINodesPrompter",
|
| 8 |
"pos": [
|
| 9 |
40,
|
| 10 |
100
|
|
|
|
| 34 |
}
|
| 35 |
],
|
| 36 |
"properties": {
|
| 37 |
+
"Node name for S&R": "DMAINodesPrompter"
|
| 38 |
},
|
| 39 |
"widgets_values": [
|
| 40 |
"{\"schema_version\": 1, \"prompt\": \"A sculptural object made of dark stone and translucent green glass, soft studio side light.\", \"negative_prompt\": \"\", \"width\": 832, \"height\": 1024, \"count\": 1}"
|
workflows/DMAI-NODES-Krea.api.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
{
|
| 2 |
"1": {
|
| 3 |
-
"class_type": "
|
| 4 |
"inputs": {
|
| 5 |
"config_json": "{\"schema_version\": 1, \"prompt\": \"A sculptural object made of dark stone and translucent green glass, soft studio side light.\", \"negative_prompt\": \"\", \"width\": 832, \"height\": 1024, \"count\": 1}"
|
| 6 |
}
|
|
|
|
| 1 |
{
|
| 2 |
"1": {
|
| 3 |
+
"class_type": "DMAINodesPrompter",
|
| 4 |
"inputs": {
|
| 5 |
"config_json": "{\"schema_version\": 1, \"prompt\": \"A sculptural object made of dark stone and translucent green glass, soft studio side light.\", \"negative_prompt\": \"\", \"width\": 832, \"height\": 1024, \"count\": 1}"
|
| 6 |
}
|
workflows/DMAI-NODES-Krea.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
| 4 |
"nodes": [
|
| 5 |
{
|
| 6 |
"id": 1,
|
| 7 |
-
"type": "
|
| 8 |
"pos": [
|
| 9 |
40,
|
| 10 |
100
|
|
@@ -34,7 +34,7 @@
|
|
| 34 |
}
|
| 35 |
],
|
| 36 |
"properties": {
|
| 37 |
-
"Node name for S&R": "
|
| 38 |
},
|
| 39 |
"widgets_values": [
|
| 40 |
"{\"schema_version\": 1, \"prompt\": \"A sculptural object made of dark stone and translucent green glass, soft studio side light.\", \"negative_prompt\": \"\", \"width\": 832, \"height\": 1024, \"count\": 1}"
|
|
|
|
| 4 |
"nodes": [
|
| 5 |
{
|
| 6 |
"id": 1,
|
| 7 |
+
"type": "DMAINodesPrompter",
|
| 8 |
"pos": [
|
| 9 |
40,
|
| 10 |
100
|
|
|
|
| 34 |
}
|
| 35 |
],
|
| 36 |
"properties": {
|
| 37 |
+
"Node name for S&R": "DMAINodesPrompter"
|
| 38 |
},
|
| 39 |
"widgets_values": [
|
| 40 |
"{\"schema_version\": 1, \"prompt\": \"A sculptural object made of dark stone and translucent green glass, soft studio side light.\", \"negative_prompt\": \"\", \"width\": 832, \"height\": 1024, \"count\": 1}"
|
workflows/DMAI-NODES-Qwen.api.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
{
|
| 2 |
"1": {
|
| 3 |
-
"class_type": "
|
| 4 |
"inputs": {
|
| 5 |
"config_json": "{\"schema_version\": 1, \"prompt\": \"A sculptural object made of dark stone and translucent green glass, soft studio side light.\", \"negative_prompt\": \"\", \"width\": 832, \"height\": 1024, \"count\": 1}"
|
| 6 |
}
|
|
|
|
| 1 |
{
|
| 2 |
"1": {
|
| 3 |
+
"class_type": "DMAINodesPrompter",
|
| 4 |
"inputs": {
|
| 5 |
"config_json": "{\"schema_version\": 1, \"prompt\": \"A sculptural object made of dark stone and translucent green glass, soft studio side light.\", \"negative_prompt\": \"\", \"width\": 832, \"height\": 1024, \"count\": 1}"
|
| 6 |
}
|
workflows/DMAI-NODES-Qwen.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
| 4 |
"nodes": [
|
| 5 |
{
|
| 6 |
"id": 1,
|
| 7 |
-
"type": "
|
| 8 |
"pos": [
|
| 9 |
40,
|
| 10 |
100
|
|
@@ -34,7 +34,7 @@
|
|
| 34 |
}
|
| 35 |
],
|
| 36 |
"properties": {
|
| 37 |
-
"Node name for S&R": "
|
| 38 |
},
|
| 39 |
"widgets_values": [
|
| 40 |
"{\"schema_version\": 1, \"prompt\": \"A sculptural object made of dark stone and translucent green glass, soft studio side light.\", \"negative_prompt\": \"\", \"width\": 832, \"height\": 1024, \"count\": 1}"
|
|
|
|
| 4 |
"nodes": [
|
| 5 |
{
|
| 6 |
"id": 1,
|
| 7 |
+
"type": "DMAINodesPrompter",
|
| 8 |
"pos": [
|
| 9 |
40,
|
| 10 |
100
|
|
|
|
| 34 |
}
|
| 35 |
],
|
| 36 |
"properties": {
|
| 37 |
+
"Node name for S&R": "DMAINodesPrompter"
|
| 38 |
},
|
| 39 |
"widgets_values": [
|
| 40 |
"{\"schema_version\": 1, \"prompt\": \"A sculptural object made of dark stone and translucent green glass, soft studio side light.\", \"negative_prompt\": \"\", \"width\": 832, \"height\": 1024, \"count\": 1}"
|
workflows/DMAI-NODES-SDXL.api.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
{
|
| 2 |
"1": {
|
| 3 |
-
"class_type": "
|
| 4 |
"inputs": {
|
| 5 |
"config_json": "{\"schema_version\": 1, \"prompt\": \"A sculptural object made of dark stone and translucent green glass, soft studio side light.\", \"negative_prompt\": \"\", \"width\": 832, \"height\": 1024, \"count\": 1}"
|
| 6 |
}
|
|
|
|
| 1 |
{
|
| 2 |
"1": {
|
| 3 |
+
"class_type": "DMAINodesPrompter",
|
| 4 |
"inputs": {
|
| 5 |
"config_json": "{\"schema_version\": 1, \"prompt\": \"A sculptural object made of dark stone and translucent green glass, soft studio side light.\", \"negative_prompt\": \"\", \"width\": 832, \"height\": 1024, \"count\": 1}"
|
| 6 |
}
|
workflows/DMAI-NODES-SDXL.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
| 4 |
"nodes": [
|
| 5 |
{
|
| 6 |
"id": 1,
|
| 7 |
-
"type": "
|
| 8 |
"pos": [
|
| 9 |
40,
|
| 10 |
100
|
|
@@ -34,7 +34,7 @@
|
|
| 34 |
}
|
| 35 |
],
|
| 36 |
"properties": {
|
| 37 |
-
"Node name for S&R": "
|
| 38 |
},
|
| 39 |
"widgets_values": [
|
| 40 |
"{\"schema_version\": 1, \"prompt\": \"A sculptural object made of dark stone and translucent green glass, soft studio side light.\", \"negative_prompt\": \"\", \"width\": 832, \"height\": 1024, \"count\": 1}"
|
|
|
|
| 4 |
"nodes": [
|
| 5 |
{
|
| 6 |
"id": 1,
|
| 7 |
+
"type": "DMAINodesPrompter",
|
| 8 |
"pos": [
|
| 9 |
40,
|
| 10 |
100
|
|
|
|
| 34 |
}
|
| 35 |
],
|
| 36 |
"properties": {
|
| 37 |
+
"Node name for S&R": "DMAINodesPrompter"
|
| 38 |
},
|
| 39 |
"widgets_values": [
|
| 40 |
"{\"schema_version\": 1, \"prompt\": \"A sculptural object made of dark stone and translucent green glass, soft studio side light.\", \"negative_prompt\": \"\", \"width\": 832, \"height\": 1024, \"count\": 1}"
|