diff --git a/AGENT-INSTALL.md b/AGENT-INSTALL.md index 60c133a..9dee001 100644 --- a/AGENT-INSTALL.md +++ b/AGENT-INSTALL.md @@ -1,6 +1,6 @@ # Installation contract for AI agents -Install MATRIX LAB NODES package version 0.2.0 as one ComfyUI custom-node pack. Treat the installed `MANIFEST.json` as the exact class inventory. +Install MATRIX LAB NODES package version 0.3.0 as one ComfyUI custom-node pack. Treat the installed `MANIFEST.json` as the exact class inventory. ## Hard boundaries @@ -19,13 +19,13 @@ Install MATRIX LAB NODES package version 0.2.0 as one ComfyUI custom-node pack. 3. Inspect `custom_nodes` for the target and for older `MATRIXLAB-Nodes` or `MATRIXLAB-UI-Nodes` installations. If any conflict exists, stop and report the exact folder names; do not modify them. 4. Obtain the exact operator-authorized commit or release archive. Record the commit SHA or SHA-256 before installation. 5. Place it in a new direct child folder `custom_nodes/matrix-lab-nodes`. Refuse archive traversal, an extra wrapper directory, symlink or junction surprises, or files outside that folder. -6. Verify that the root contains `__init__.py`, `MANIFEST.json`, `requirements.txt`, `_core/`, `nodes/`, and `web/`. Read `MANIFEST.json`; require `pack_id` `matrix-lab-nodes`, version `0.2.0`, fourteen unique class IDs, and six category groups. +6. Verify that the root contains `__init__.py`, `MANIFEST.json`, `requirements.txt`, `_core/`, `nodes/`, and `web/`. Read `MANIFEST.json`; require `pack_id` `matrix-lab-nodes`, version `0.3.0`, fourteen unique class IDs, and six category groups. 7. Resolve the declared Python dependencies with the same interpreter that runs ComfyUI. Preserve an already working Torch/CUDA stack. If dependency resolution would replace Torch, CUDA packages, or another host-critical package, stop and present the proposed changes. 8. Restart ComfyUI through its normal launcher. Treat any pack import or frontend asset error as failure. 9. In the visible UI, search for `MATRIX RESOLUTION`, `MATRIX PHOTO FINISHER`, and `MATRIX AUTO PROMPTER`; compare all discovered MATRIX classes with `MANIFEST.json`. -10. Add `MATRIXLAB_Resolution`, choose `3:4` and `2K`, and verify 1536 by 2048. Do not queue any model, detector, sampler, or provider path. +10. Add `MATRIX_Resolution`, choose `3:4` and `2K`, and verify 1536 by 2048. Do not queue any model, detector, sampler, or provider path. 11. Report the installed source/hash, target folder, manifest version and classes, dependency actions, visible discovery result, geometry result, and every blocker. Distinguish offline verification from unperformed live model/renderer acceptance. ## Credential and private-data boundary -The Prompt Director credential must be supplied only by the operator through the masked node control or server environment. Never inspect an existing provider key or test it during installation. Do not upload images, prompts, workflows, logs, or screenshots. Diagnostics must exclude credentials, local paths, private media, prompt text, and provider response bodies. +The Auto Prompter credential must be supplied only by the operator through the masked node control or server environment. Never inspect an existing provider key or test it during installation. Do not upload images, prompts, workflows, logs, or screenshots. Diagnostics must exclude credentials, local paths, private media, prompt text, and provider response bodies. diff --git a/CHANGELOG.md b/CHANGELOG.md index 92dd4fc..f394aaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ User-visible changes are recorded here. This file describes package content; publication tags and release channels are separate facts. +## 0.3.0 - development candidate - 2026-09-09 + +- Standardized eight public class IDs: `MATRIX_MetadataKiller`, `MATRIX_AutoPrompter`, `MATRIX_ImageBatchLoader`, `MATRIX_Resolution`, `MATRIX_AIInfluencerResolution`, `MATRIX_AIInfluencerResolution2K4K`, `MATRIX_EasyCrop`, and `MATRIX_SpectralSampler`. +- Kept the package at fourteen registered nodes; the eight old IDs are not registered as aliases. +- Added an offline workflow migration path that writes a separate copy and changes only the eight class IDs. +- Standardized the user-facing name as Auto Prompter. + +This is a breaking workflow-identity change. Read [docs/migration.md](docs/migration.md) before opening 0.2.0 workflows with the new package. + ## 0.2.0 - development candidate - 2026-09-09 - Unified the surviving execution and UI nodes into one package with six stable category directories. @@ -9,7 +18,7 @@ User-visible changes are recorded here. This file describes package content; pub - Replaced the retired finishing pair with `MATRIX_PhotoFinisher`, a local deterministic Torch node with `Clean Digital`, `Everyday Capture`, and `Low Light` creative profiles. - Set new Photo Finisher defaults to `Everyday Capture`, mix `1`, texture `1`, detail `1`, contrast `0`, warmth `0`, saturation `1`, and seed `42`. - Simplified the general and AI-influencer resolution nodes to two integer outputs and included `MATRIXLAB_AIInfluencerResolution2K4K` for current Krea 2 geometry selection. -- Kept Prompt Director provider generation behind an explicit paid action; ordinary graph execution returns saved text without a network request. +- Kept Auto Prompter provider generation behind an explicit paid action; ordinary graph execution returns saved text without a network request. - Added public installation, migration, compatibility, node, license-notice, and agent-installation documentation. Full live acceptance of the current bytes across both ComfyUI renderers and all model-dependent paths remains pending. diff --git a/INSTALL.md b/INSTALL.md index 65b6408..4a89fae 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -1,6 +1,6 @@ # Installation for people -Install MATRIX LAB NODES package version 0.2.0 as one ComfyUI custom-node pack. +Install MATRIX LAB NODES package version 0.3.0 as one ComfyUI custom-node pack. ## Before you begin @@ -38,7 +38,7 @@ Extract the release archive to a new folder named `matrix-lab-nodes` directly un 1. Start ComfyUI through its normal launcher and inspect startup output for import errors. 2. Open node search and find `MATRIX RESOLUTION`, `MATRIX PHOTO FINISHER`, and `MATRIX AUTO PROMPTER`. 3. Compare the installed classes with `MANIFEST.json`. The menu should retain six `MATRIX LAB` category groups. -4. Add `MATRIXLAB_Resolution`, select `3:4` and `2K`, and confirm outputs of 1536 by 2048 without queueing a model workflow. +4. Add `MATRIX_Resolution`, select `3:4` and `2K`, and confirm outputs of 1536 by 2048 without queueing a model workflow. 5. Open a copy of an existing workflow and follow [docs/migration.md](docs/migration.md) if ComfyUI reports missing retired or incompatible classes. This verification proves discovery and the local geometry path only. It does not prove every model, detector, sampler, GPU, or frontend renderer. @@ -47,7 +47,7 @@ This verification proves discovery and the local geometry path only. It does not Model weights are not bundled. Installing the pack does not acquire them, establish redistribution rights, or complete compatible inference-runtime setup. `MATRIX_SkinMask` and `MATRIX_EyeMask` fail when their registered assets or compatible runtimes are unavailable; do not bypass identity checks with a similarly named file. A successful package import is not proof that either mask node is ready to execute. Follow [the detector and segmentation setup guide](docs/detector-setup.md) for exact paths, hashes, source evidence, runtime boundaries, and unresolved license questions. -`MATRIXLAB_PromptDirector` returns saved text during normal execution. Its explicit **Generate Prompt** control uses xAI and may incur charges. Configure credentials through its masked control or the documented server environment, never inside a workflow. Make the first provider call only after reviewing the selected model, reference images, request text, and cost boundary. +`MATRIX_AutoPrompter` returns saved text during normal execution. Its explicit **Generate Prompt** control uses xAI and may incur charges. Configure credentials through its masked control or the documented server environment, never inside a workflow. Make the first provider call only after reviewing the selected model, reference images, request text, and cost boundary. ## Update diff --git a/MANIFEST.json b/MANIFEST.json index 1733ce6..b042b73 100644 --- a/MANIFEST.json +++ b/MANIFEST.json @@ -21,7 +21,7 @@ "version": "1.0.0" }, { - "content_hash": "dcaf1a3ae2fc7ac78413342300aea580751b42ca019414b5cbb3c205a43d1611", + "content_hash": "177c945bf563a1adeb831a8b87d268725fc9dd10bb423b51e1f16704d9fe3f2f", "id": "ui.appearance", "version": "0.5.1" }, @@ -31,7 +31,7 @@ "version": "0.3.1" }, { - "content_hash": "f07c6307623d28a8e8ba43307d254f53809a40af5da1556a762747248e64cd3f", + "content_hash": "46dd2a9579be353e12d69cb725bb76c5aa2298032d06fc05801f6e14e4189305", "id": "sample.latent-tail", "version": "0.2.3" }, @@ -66,7 +66,7 @@ "version": "0.1.0" }, { - "content_hash": "869da644f94fb10c804e477bb2bd8062ffdab3545855f52f56580dbf5ad4f47f", + "content_hash": "cdb207f60ea7c8722de257b0f418dad81bc634df8e679aa4efd460708570a9d7", "id": "resolution.dimensions", "version": "1.0.0" }, @@ -76,12 +76,12 @@ "version": "1.0.0" }, { - "content_hash": "8b9ef9d731c147c5b9491e28f26c5fc5a6d2e4e2be86596b80e16d16d662e999", + "content_hash": "86190b4be8b5080a8db60f030c7da5173b45cce0fd8ad72f22966355934cfc0f", "id": "easy.crop", "version": "0.1.0" }, { - "content_hash": "4e67bd73efebed6140e3531e5cf8a95f06d60d174f66afbbeb91e2c3c4e34e03", + "content_hash": "befa0b792f813d347f69cdaa1711760faa5cbf9e5e0011b893d534d17c34866d", "id": "io.image-collection", "version": "0.2.1" }, @@ -115,75 +115,75 @@ "version": "1.0.0" } ], - "builder_sha256": "5bc6cffaa3575a987e2e2204aff80bbad20307fdad130c9f83562bd902bee220", + "builder_sha256": "831ce90d7ec059f0538c8b690e2078339fddd81ecf8197fb8ec4767f920606b4", "categories": { - "MATRIXLAB_AIInfluencerResolution": "MATRIX LAB/Resolution & Layout", - "MATRIXLAB_EasyCrop": "MATRIX LAB/Image Processing", - "MATRIXLAB_ImageBatchLoader": "MATRIX LAB/Input & Output", - "MATRIXLAB_PromptDirector": "MATRIX LAB/Prompting", - "MATRIXLAB_Resolution": "MATRIX LAB/Resolution & Layout", - "MATRIXSpectralSampler": "MATRIX LAB/Sampling & Detail", + "MATRIX_AIInfluencerResolution": "MATRIX LAB/Resolution & Layout", + "MATRIX_AutoPrompter": "MATRIX LAB/Prompting", "MATRIX_CropTailPaste": "MATRIX LAB/Sampling & Detail", + "MATRIX_EasyCrop": "MATRIX LAB/Image Processing", "MATRIX_EyeMask": "MATRIX LAB/Masks & Detection", + "MATRIX_ImageBatchLoader": "MATRIX LAB/Input & Output", "MATRIX_LatentTail": "MATRIX LAB/Sampling & Detail", + "MATRIX_MetadataKiller": "MATRIX LAB/Input & Output", "MATRIX_OutputStage": "MATRIX LAB/Image Processing", "MATRIX_PhotoFinisher": "MATRIX LAB/Image Processing", - "MATRIX_SaveClean": "MATRIX LAB/Input & Output", + "MATRIX_Resolution": "MATRIX LAB/Resolution & Layout", "MATRIX_SkinMask": "MATRIX LAB/Masks & Detection", - "MATRIXLAB_AIInfluencerResolution2K4K": "MATRIX LAB/Resolution & Layout" + "MATRIX_SpectralSampler": "MATRIX LAB/Sampling & Detail", + "MATRIX_AIInfluencerResolution2K4K": "MATRIX LAB/Resolution & Layout" }, - "compiler_sha256": "b1ff75cd7cefd07caaf7fc224d2b4518d7a65954c89e3bdee770d867a37e1e07", + "compiler_sha256": "deb114d7e9674a9c2ec2a24b9d7c95e008eeee17fc74e7da808805db8fb76469", "declaration_hashes": { - "MATRIXLAB_AIInfluencerResolution": "8f6737741244227d88ed8a411548fd788fe311f776ca9aba68b67f34bf9695d9", - "MATRIXLAB_EasyCrop": "e3e4d55649ba8fd8d1d5fb2abc526af8ca98b3b7a5b62665965eed080b0f3c2a", - "MATRIXLAB_ImageBatchLoader": "bd6b56231c6d2842fb044ff7c5a2c775ca2043837a5879eff9a397569b153319", - "MATRIXLAB_PromptDirector": "bbebce94b066143c8879239fa0003fc934207bebed40c6352d9d9bba882e54e4", - "MATRIXLAB_Resolution": "38fea06eda599cb3c78a029b8e0478b3625bc1cd281019caec6cd9612a3ff521", - "MATRIXSpectralSampler": "767c07ee23b36895169d5039756f8f425ef0aa0e70ca639d616b5af75c829306", + "MATRIX_AIInfluencerResolution": "9dd100da79f07f132edda489d044fc2389017ac74ec7401f33d63f0f2ece3ce7", + "MATRIX_AutoPrompter": "1f9b62b8c5b9d5eba5596aeb3493b1d62b7978b5a989a904f09f5bf1af9006b9", "MATRIX_CropTailPaste": "7c0a5e925c00a9a08d445e9b8c9315909f17084d9279c37f3841cbce9c2670c7", + "MATRIX_EasyCrop": "4a14271978355ecc30a172361893f3fdacf5c62276b8440bb0be2e59fbb1c3bf", "MATRIX_EyeMask": "c507e7a4ca5c6b959ab503b4229ec929856576567bc28a82b06e8f5cabebd7ef", + "MATRIX_ImageBatchLoader": "ba73458404c99b072fc42e28cdfae304066657c63ca3e729bae5d5fd6bc60ffb", "MATRIX_LatentTail": "8677264027158b709269b0ffe22c4ce7a9b81862f78d5d9057f964a0f362f83e", + "MATRIX_MetadataKiller": "2a96e38815d0f863aa777546231a8ca78640240f7478f523316aaf19d5e210b2", "MATRIX_OutputStage": "6ec9c264ab82dfd9b8c0e8640007a26db25818b3bf7a934db34e7403ba441bbc", "MATRIX_PhotoFinisher": "eadad3bc1712edec30fa52fff0a929e4807a77b17b60fd5e51b4c4403d7bf809", - "MATRIX_SaveClean": "3aaeea9d3b5b246e909df9a21ae9f2883bf251c055dcc9f8e89714beae65a3ce", + "MATRIX_Resolution": "7227bb19186a7f307e05928b4f997b70ff41434003c63795510204cd5c7eb948", "MATRIX_SkinMask": "e17b320fdc4ab7535fb6f017578e43b87e6f8d90740adfba2aeabe1a377521f2", - "MATRIXLAB_AIInfluencerResolution2K4K": "589e6e5dc7c7eb7ee22943b13729bf2012350baec8c4b35b0320badcbfd6b22c" + "MATRIX_SpectralSampler": "065dea25f4f05c00f0bc920da0655b493a111378cef5e897f1b91ded7d463a43", + "MATRIX_AIInfluencerResolution2K4K": "78dfa19ace9ef16911c70150e9bd81b208f9fbeda89acad62a0d4e18668438ef" }, "display_name": "MATRIX LAB NODES", "frontend_entrypoints": [ - "web/ai_influencer_resolution.bd473fd081a9ee32.js", - "web/ai_influencer_resolution_2k4k.5a6f0a9d0be56983.js", - "web/easy_crop.e448d80a597cbf32.js", - "web/halo_execution.9c05a384226d4180.js", - "web/image_collection.57c26c1112198635.js", - "web/prompt_director.47d51c1218485744.js", - "web/resolution_control_deck.83ec2d2da93e1ef9.js" + "web/ai_influencer_resolution.d47e945783aa6298.js", + "web/ai_influencer_resolution_2k4k.203f0bdb857d180d.js", + "web/easy_crop.4fae2e79d5ac41d4.js", + "web/halo_execution.615447e8edacbd1a.js", + "web/image_collection.4a857a26ffb9735e.js", + "web/prompt_director.402dd6f04f072edf.js", + "web/resolution_control_deck.11b94ed50b65d983.js" ], "nodes": [ - "MATRIXLAB_AIInfluencerResolution", - "MATRIXLAB_AIInfluencerResolution2K4K", - "MATRIXLAB_EasyCrop", - "MATRIXLAB_ImageBatchLoader", - "MATRIXLAB_PromptDirector", - "MATRIXLAB_Resolution", - "MATRIXSpectralSampler", + "MATRIX_AIInfluencerResolution", + "MATRIX_AIInfluencerResolution2K4K", + "MATRIX_AutoPrompter", "MATRIX_CropTailPaste", + "MATRIX_EasyCrop", "MATRIX_EyeMask", + "MATRIX_ImageBatchLoader", "MATRIX_LatentTail", + "MATRIX_MetadataKiller", "MATRIX_OutputStage", "MATRIX_PhotoFinisher", - "MATRIX_SaveClean", - "MATRIX_SkinMask" + "MATRIX_Resolution", + "MATRIX_SkinMask", + "MATRIX_SpectralSampler" ], "pack_id": "matrix-lab-nodes", "runtime_asset_registry_sha256": "ab1955cfe5e8d05b832a4f63cae121d9afd6f74d3a9048b0107cf634659cada8", "status": "development", - "version": "0.2.0", + "version": "0.3.0", "candidate_provenance": { "strategy": "current-canonical-base-plus-additive-factory-node", - "base_builder_sha256": "5bc6cffaa3575a987e2e2204aff80bbad20307fdad130c9f83562bd902bee220", - "compiler_sha256": "b1ff75cd7cefd07caaf7fc224d2b4518d7a65954c89e3bdee770d867a37e1e07", - "builder_sha256": "7df54e744e3aff1b58fd9699f5e1564e3f0c71d8370fe8b50b07aedf74dbeca9", + "base_builder_sha256": "831ce90d7ec059f0538c8b690e2078339fddd81ecf8197fb8ec4767f920606b4", + "compiler_sha256": "deb114d7e9674a9c2ec2a24b9d7c95e008eeee17fc74e7da808805db8fb76469", + "builder_sha256": "f60506d9d5a1597c860f08d6b8a8119a265b4e50ef42e4930defc60e9bc55ab3", "existing_node_implementation_policy": "fresh-canonical-base-build", "runtime_registry_policy": "owned-by-canonical-base-builder", "runtime_asset_proof_sha256": "917902e05bb4e0758e6ba9b264075f89edbc0b2b39580fb2cb55d6e9c76945f8" diff --git a/NODES.md b/NODES.md index 54f5312..7cde61e 100644 --- a/NODES.md +++ b/NODES.md @@ -1,22 +1,22 @@ # Node reference -Package version 0.2.0 contains the fourteen classes below in six `MATRIX LAB` groups. The installed `MANIFEST.json` is authoritative for a particular artifact. +Package version 0.3.0 contains the fourteen classes below in six `MATRIX LAB` groups. The installed `MANIFEST.json` is authoritative for a particular artifact. | Display name | Class ID | Inputs | Outputs | Guide | | --- | --- | --- | --- | --- | -| MATRIX METADATA KILLER | `MATRIX_SaveClean` | `images`; save options | output node | [Guide](docs/nodes/matrix-save-clean.md) | -| MATRIX IMAGE BATCH LOADER | `MATRIXLAB_ImageBatchLoader` | `collection` | `images`, `masks` | [Guide](docs/nodes/image-batch-loader.md) | -| MATRIX RESOLUTION | `MATRIXLAB_Resolution` | ratio, tier, custom dimensions | `width`, `height` | [Guide](docs/nodes/resolution.md) | -| MATRIX AI INFLUENCER RESOLUTION | `MATRIXLAB_AIInfluencerResolution` | ratio, tier | `width`, `height` | [Guide](docs/nodes/ai-influencer-resolution.md) | -| MATRIX AI INFLUENCER RESOLUTION 2K/4K | `MATRIXLAB_AIInfluencerResolution2K4K` | ratio, 2K/4K tier | `width`, `height` | [Guide](docs/nodes/ai-influencer-resolution-2k4k.md) | -| MATRIX SPECTRAL SAMPLER | `MATRIXSpectralSampler` | sampler and spectral controls | `sampler` | [Guide](docs/nodes/spectral-sampler.md) | +| MATRIX METADATA KILLER | `MATRIX_MetadataKiller` | `images`; save options | output node | [Guide](docs/nodes/metadata-killer.md) | +| MATRIX IMAGE BATCH LOADER | `MATRIX_ImageBatchLoader` | `collection` | `images`, `masks` | [Guide](docs/nodes/image-batch-loader.md) | +| MATRIX RESOLUTION | `MATRIX_Resolution` | ratio, tier, custom dimensions | `width`, `height` | [Guide](docs/nodes/resolution.md) | +| MATRIX AI INFLUENCER RESOLUTION | `MATRIX_AIInfluencerResolution` | ratio, tier | `width`, `height` | [Guide](docs/nodes/ai-influencer-resolution.md) | +| MATRIX AI INFLUENCER RESOLUTION 2K/4K | `MATRIX_AIInfluencerResolution2K4K` | ratio, 2K/4K tier | `width`, `height` | [Guide](docs/nodes/ai-influencer-resolution-2k4k.md) | +| MATRIX SPECTRAL SAMPLER | `MATRIX_SpectralSampler` | sampler and spectral controls | `sampler` | [Guide](docs/nodes/spectral-sampler.md) | | MATRIX LATENT TAIL | `MATRIX_LatentTail` | model/noise/conditioning/latent; tail controls | `latent` | [Guide](docs/nodes/latent-tail.md) | | MATRIX CROP TAIL PASTE | `MATRIX_CropTailPaste` | image/mask/model/noise/conditioning/VAE; crop controls | `image` | [Guide](docs/nodes/crop-tail-paste.md) | | MATRIX SKIN MASK | `MATRIX_SkinMask` | image; part and mask controls | `mask`, `preview` | [Guide](docs/nodes/skin-mask.md) | | MATRIX EYE MASK | `MATRIX_EyeMask` | image; detector and mask controls | `mask`, `masks`, `bboxes`, `preview` | [Guide](docs/nodes/eye-mask.md) | | MATRIX PHOTO FINISHER | `MATRIX_PhotoFinisher` | image; optional mask and finish controls | `image` | [Guide](docs/nodes/photo-finisher.md) | -| MATRIX EASY CROP | `MATRIXLAB_EasyCrop` | uploaded image and crop geometry | `image`, `mask` | [Guide](docs/nodes/easy-crop.md) | +| MATRIX EASY CROP | `MATRIX_EasyCrop` | uploaded image and crop geometry | `image`, `mask` | [Guide](docs/nodes/easy-crop.md) | | MATRIX OUTPUT STAGE | `MATRIX_OutputStage` | image/model/noise/conditioning/VAE/target geometry; output controls | `image`, `info` | [Guide](docs/nodes/output-stage.md) | -| MATRIX AUTO PROMPTER | `MATRIXLAB_PromptDirector` | saved prompt fields; optional images | `prompt` | [Guide](docs/nodes/auto-prompter.md) | +| MATRIX AUTO PROMPTER | `MATRIX_AutoPrompter` | saved prompt fields; optional images | `prompt` | [Guide](docs/nodes/auto-prompter.md) | -`MATRIXLAB_AIInfluencerResolution2K4K` does not replace the base AI Influencer Resolution class. +`MATRIX_AIInfluencerResolution2K4K` does not replace the base AI Influencer Resolution class. diff --git a/README.md b/README.md index 7b6e64b..60e83c9 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Fourteen focused ComfyUI nodes for image input, resolution, sampling, masks, finishing, output, and assisted prompting—organized in six consistent `MATRIX LAB` categories. -> **Development candidate 0.2.0.** Read the current [compatibility and acceptance boundary](docs/compatibility.md) before using it in an important workflow. +> **Development candidate 0.3.0.** Read the current [compatibility and acceptance boundary](docs/compatibility.md) before using it in an important workflow. ## Installation @@ -16,7 +16,7 @@ Install this unified pack as one folder under ComfyUI's `custom_nodes`. Do not e ## Input & Output (2) - [**MATRIX IMAGE BATCH LOADER**](docs/nodes/image-batch-loader.md) — load and reorder one to ten static input images while preserving each image's dimensions. -- [**MATRIX METADATA KILLER**](docs/nodes/matrix-save-clean.md) — save complete image batches as clean JPEG or PNG output without prompt or workflow metadata inputs. +- [**MATRIX METADATA KILLER**](docs/nodes/metadata-killer.md) — save complete image batches as clean JPEG or PNG output without prompt or workflow metadata inputs. ## Resolution & Layout (3) diff --git a/SECURITY.md b/SECURITY.md index a00339c..cd0dfbb 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,6 +2,6 @@ Do not include credentials, private prompts, workflows, local paths, provider response bodies, or private media in public reports. -`MATRIXLAB_PromptDirector` keeps provider credentials outside serialized workflows. Ordinary node execution returns saved text and makes no provider request. Only the explicit **Generate Prompt** action may send selected reference images and prompt fields to xAI. Review that request before authorizing provider use. +`MATRIX_AutoPrompter` keeps provider credentials outside serialized workflows. Ordinary node execution returns saved text and makes no provider request. Only the explicit **Generate Prompt** action may send selected reference images and prompt fields to xAI. Review that request before authorizing provider use. When reporting a vulnerability, use the repository's Security tab if private reporting is available. Otherwise, open a minimal issue containing no secrets, private data, or working exploit and request a private contact route. diff --git a/__init__.py b/__init__.py index f0e3494..d160f74 100644 --- a/__init__.py +++ b/__init__.py @@ -1,16 +1,16 @@ """Compiled ComfyUI node pack.""" -from .nodes.sampling_detail.matrixspectralsampler import NODE_CLASS_MAPPINGS as _c0, NODE_DISPLAY_NAME_MAPPINGS as _d0 +from .nodes.sampling_detail.matrix_spectralsampler import NODE_CLASS_MAPPINGS as _c0, NODE_DISPLAY_NAME_MAPPINGS as _d0 from .nodes.sampling_detail.matrix_latenttail import NODE_CLASS_MAPPINGS as _c1, NODE_DISPLAY_NAME_MAPPINGS as _d1 from .nodes.masks_detection.matrix_skinmask import NODE_CLASS_MAPPINGS as _c2, NODE_DISPLAY_NAME_MAPPINGS as _d2 from .nodes.masks_detection.matrix_eyemask import NODE_CLASS_MAPPINGS as _c3, NODE_DISPLAY_NAME_MAPPINGS as _d3 from .nodes.sampling_detail.matrix_croptailpaste import NODE_CLASS_MAPPINGS as _c4, NODE_DISPLAY_NAME_MAPPINGS as _d4 -from .nodes.input_output.matrix_saveclean import NODE_CLASS_MAPPINGS as _c5, NODE_DISPLAY_NAME_MAPPINGS as _d5 +from .nodes.input_output.matrix_metadatakiller import NODE_CLASS_MAPPINGS as _c5, NODE_DISPLAY_NAME_MAPPINGS as _d5 from .nodes.image_processing.matrix_outputstage import NODE_CLASS_MAPPINGS as _c6, NODE_DISPLAY_NAME_MAPPINGS as _d6 from .nodes.image_processing.matrix_photofinisher import NODE_CLASS_MAPPINGS as _c7, NODE_DISPLAY_NAME_MAPPINGS as _d7 -from .nodes.resolution_layout.matrixlab_resolution import NODE_CLASS_MAPPINGS as _c8, NODE_DISPLAY_NAME_MAPPINGS as _d8 -from .nodes.resolution_layout.matrixlab_aiinfluencerresolution import NODE_CLASS_MAPPINGS as _c9, NODE_DISPLAY_NAME_MAPPINGS as _d9 -from .nodes.image_processing.matrixlab_easycrop import NODE_CLASS_MAPPINGS as _c10, NODE_DISPLAY_NAME_MAPPINGS as _d10 -from .nodes.input_output.matrixlab_imagebatchloader import NODE_CLASS_MAPPINGS as _c11, NODE_DISPLAY_NAME_MAPPINGS as _d11 +from .nodes.resolution_layout.matrix_resolution import NODE_CLASS_MAPPINGS as _c8, NODE_DISPLAY_NAME_MAPPINGS as _d8 +from .nodes.resolution_layout.matrix_aiinfluencerresolution import NODE_CLASS_MAPPINGS as _c9, NODE_DISPLAY_NAME_MAPPINGS as _d9 +from .nodes.image_processing.matrix_easycrop import NODE_CLASS_MAPPINGS as _c10, NODE_DISPLAY_NAME_MAPPINGS as _d10 +from .nodes.input_output.matrix_imagebatchloader import NODE_CLASS_MAPPINGS as _c11, NODE_DISPLAY_NAME_MAPPINGS as _d11 NODE_CLASS_MAPPINGS = {} NODE_DISPLAY_NAME_MAPPINGS = {} @@ -39,7 +39,7 @@ NODE_CLASS_MAPPINGS.update(_c11) NODE_DISPLAY_NAME_MAPPINGS.update(_d11) -from .nodes.resolution_layout.matrixlab_aiinfluencerresolution2k4k import NODE_CLASS_MAPPINGS as _resolution_2k4k_c, NODE_DISPLAY_NAME_MAPPINGS as _resolution_2k4k_d +from .nodes.resolution_layout.matrix_aiinfluencerresolution2k4k import NODE_CLASS_MAPPINGS as _resolution_2k4k_c, NODE_DISPLAY_NAME_MAPPINGS as _resolution_2k4k_d NODE_CLASS_MAPPINGS.update(_resolution_2k4k_c) NODE_DISPLAY_NAME_MAPPINGS.update(_resolution_2k4k_d) @@ -54,8 +54,8 @@ WEB_DIRECTORY = "./web" __all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"] -from .nodes.prompting.matrixlab_promptdirector import MATRIXLAB_PromptDirector -NODE_CLASS_MAPPINGS['MATRIXLAB_PromptDirector'] = MATRIXLAB_PromptDirector -NODE_DISPLAY_NAME_MAPPINGS['MATRIXLAB_PromptDirector'] = 'MATRIX AUTO PROMPTER' +from .nodes.prompting.matrix_autoprompter import MATRIX_AutoPrompter +NODE_CLASS_MAPPINGS['MATRIX_AutoPrompter'] = MATRIX_AutoPrompter +NODE_DISPLAY_NAME_MAPPINGS['MATRIX_AutoPrompter'] = 'MATRIX AUTO PROMPTER' from ._core.prompt_director.server_routes import register_routes as _register_prompt_director _register_prompt_director() diff --git a/_core/prompt_director/__init__.py b/_core/prompt_director/__init__.py index ad28939..cd37d46 100644 --- a/_core/prompt_director/__init__.py +++ b/_core/prompt_director/__init__.py @@ -8,7 +8,7 @@ DEFAULT_INSTRUCTIONS = "Create one coherent photorealistic Krea 2 prompt from these references." -class MATRIXLAB_PromptDirector: +class MATRIX_AutoPrompter: @classmethod def INPUT_TYPES(cls): fields = { @@ -50,4 +50,4 @@ def execute( from .server_routes import PromptDirectorService, load_model_configs, register_routes -__all__ = ["DEFAULT_INSTRUCTIONS", "DEFAULT_MODEL", "EMPTY_GENERATION_STATE", "MATRIXLAB_PromptDirector", "PromptDirectorService", "SYSTEM_PROMPT", "load_model_configs", "register_routes"] +__all__ = ["DEFAULT_INSTRUCTIONS", "DEFAULT_MODEL", "EMPTY_GENERATION_STATE", "MATRIX_AutoPrompter", "PromptDirectorService", "SYSTEM_PROMPT", "load_model_configs", "register_routes"] diff --git a/_core/sample_latent_tail/__init__.py b/_core/sample_latent_tail/__init__.py index 30bb976..20cc92e 100644 --- a/_core/sample_latent_tail/__init__.py +++ b/_core/sample_latent_tail/__init__.py @@ -40,7 +40,7 @@ _LOG = logging.getLogger("MATRIX.LatentTail") -# Pinned ComfyUI Core inventory (0.33 line); the same list MATRIXSpectralSampler ships. +# Pinned ComfyUI Core inventory (0.33 line); the same list MATRIX_SpectralSampler ships. CORE_SAMPLER_NAMES = ( "euler", "euler_cfg_pp", diff --git a/docs/compatibility.md b/docs/compatibility.md index 64883aa..32e5523 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -2,7 +2,7 @@ ## Current status -Version 0.2.0 is a development candidate. Offline checks cover package registration, schemas, category layout, deterministic builds, runtime registry integrity, and frontend syntax. Those checks do not establish complete live acceptance for Classic and Nodes 2.0 renderers, every interaction, every GPU/model combination, or production use. +Version 0.3.0 is a development candidate. Offline checks cover package registration, schemas, category layout, deterministic builds, runtime registry integrity, and frontend syntax. Those checks do not establish complete live acceptance for Classic and Nodes 2.0 renderers, every interaction, every GPU/model combination, or production use. | Area | Current boundary | | --- | --- | @@ -14,7 +14,7 @@ Version 0.2.0 is a development candidate. Offline checks cover package registrat | Skin Mask | Needs separately acquired, licensed, and configured segmentation assets plus compatible runtimes. Installation does not complete this setup. An all-parts-off run can return an empty mask without inference. | | Eye Mask | Needs the registered eye detector and, when enabled, SAM refinement assets/runtime. Model weights are not bundled; installation does not complete this setup. | | Sampling/detail | Requires compatible ComfyUI MODEL, NOISE, CONDITIONING, LATENT, VAE, SAMPLER, or UPSCALE_MODEL inputs as documented per node. Sampler availability follows the host ComfyUI contract. | -| Prompt Director | Ordinary graph execution is local. Explicit Generate Prompt requires a supported xAI credential/model and may be paid. | +| Auto Prompter | Ordinary graph execution is local. Explicit Generate Prompt requires a supported xAI credential/model and may be paid. | | Platforms | No broad Windows, Linux, macOS, cloud, or portable-build support claim is made until each environment is accepted with the exact release bytes. | ## External assets diff --git a/docs/migration.md b/docs/migration.md index 4c2b186..5a441a5 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -1,6 +1,31 @@ -# Migration to the unified package +# Migration to 0.3.0 -Version 0.2.0 replaces two older packs with one package. Back up workflows and the existing installations before switching. The older `MATRIXLAB-Nodes` and `MATRIXLAB-UI-Nodes` packs must remain disabled while this package is enabled because retained class IDs otherwise register twice. +Version 0.3.0 deliberately standardizes eight class IDs. It still contains fourteen registered nodes in six categories. The old IDs are not registered as aliases, so existing workflow JSON must be migrated before those nodes can load. + +## Class ID mapping + +| 0.2.0 class ID | 0.3.0 class ID | +| --- | --- | +| `MATRIX_SaveClean` | `MATRIX_MetadataKiller` | +| `MATRIXLAB_PromptDirector` | `MATRIX_AutoPrompter` | +| `MATRIXLAB_ImageBatchLoader` | `MATRIX_ImageBatchLoader` | +| `MATRIXLAB_Resolution` | `MATRIX_Resolution` | +| `MATRIXLAB_AIInfluencerResolution` | `MATRIX_AIInfluencerResolution` | +| `MATRIXLAB_AIInfluencerResolution2K4K` | `MATRIX_AIInfluencerResolution2K4K` | +| `MATRIXLAB_EasyCrop` | `MATRIX_EasyCrop` | +| `MATRIXSpectralSampler` | `MATRIX_SpectralSampler` | + +## Safe offline migration + +Back up workflows before switching packages. Run the repository tool offline and write to a new output file: + +```bash +python tools/migrate_workflow.py --input old.json --output new.json +``` + +Keep `old.json` unchanged. Do not use the same path for input and output, overwrite an existing migration result, or batch-replace arbitrary strings by hand. The tool duplicates the workflow and rewrites only the eight class IDs in the table. Open `new.json` in ComfyUI, inspect every migrated node and connection, then save and reload before execution. + +The older `MATRIXLAB-Nodes` and `MATRIXLAB-UI-Nodes` packs must remain disabled while version 0.3.0 is enabled because overlapping historical installations can register conflicting classes. ## Finishing-node replacement @@ -19,13 +44,13 @@ There is no exact mapping for the old ISO, JPEG, or motion controls. Photo Finis ## Resolution changes -`MATRIXLAB_Resolution` now returns only `width` and `height`. Its canonical controls are `aspect_ratio`, `resolution_tier`, `custom_width`, and `custom_height`. A former `max_side` or `divisible_by` value cannot be copied by widget position. Recreate the node and reconnect outputs by name and meaning. +`MATRIX_Resolution` returns only `width` and `height`. Its canonical controls are `aspect_ratio`, `resolution_tier`, `custom_width`, and `custom_height`. A former `max_side` or `divisible_by` value cannot be copied by widget position. Recreate the node and reconnect outputs by name and meaning. -`MATRIXLAB_AIInfluencerResolution` also returns only `width` and `height`. Older saved versions exposed six render, delivery, sampler-scale, and plan outputs. Replace those nodes manually and review every downstream link because output indices no longer share the same meaning. The current `1K` choice means an exact 1024-pixel longer edge. +`MATRIX_AIInfluencerResolution` also returns only `width` and `height`. Older saved versions exposed six render, delivery, sampler-scale, and plan outputs. Replace those nodes manually and review every downstream link because output indices no longer share the same meaning. The current `1K` choice means an exact 1024-pixel longer edge. -`MATRIX_ResolutionPlan`, `MATRIXLAB_Resolution2K`, and `MATRIXLAB_Resolution4KProgressive` are not included. Workflows using them require deliberate replacement. `MATRIXLAB_AIInfluencerResolution2K4K` is a two-output geometry selector and is not an ABI-compatible substitute for the older connected model/sampler nodes. +`MATRIX_ResolutionPlan`, `MATRIXLAB_Resolution2K`, and `MATRIXLAB_Resolution4KProgressive` are not included. Workflows using them require deliberate replacement. `MATRIX_AIInfluencerResolution2K4K` is a two-output geometry selector and is not an ABI-compatible substitute for the older connected model/sampler nodes. -Current Krea 2 graphs that still contain `MATRIX_CameraLook` or `MATRIX_Renoise` are not drop-in compatible with this package. Migrate those graphs manually before treating the unified pack as their replacement. +Current Krea 2 graphs that still contain `MATRIX_CameraLook` or `MATRIX_Renoise` are not drop-in compatible with this package. The offline tool does not rewrite either class because replacing them requires a semantic decision. Replace them manually with `MATRIX_PhotoFinisher`, reconnect the image path, and retune the finish before treating the unified pack as their replacement. ## Retained class IDs @@ -33,4 +58,6 @@ Other base classes retain their class IDs, but widget schemas and runtime assets ## Rollback reference -The legacy predecessor revision is `eaa50a674005b4e7bf7be13f0c289faeb37bfcef`. Keep the old installation disabled and intact until migrated workflows pass. Restore that exact revision as a separate rollback operation; do not copy shared `_core`, `nodes`, or `web` folders between versions. +For the pre-rename 0.2.0 package, use tag `legacy-class-ids-0.2.0-2026-09-09` at revision `0e798eac7d6569ad183ce0a036b81e63f0d7304e`. + +The older Camera Look/Renoise predecessor is preserved by tag `legacy-camera-renoise-2026-09-09`. Its revision is `eaa50a674005b4e7bf7be13f0c289faeb37bfcef`. Keep the old installation disabled and intact until migrated workflows pass. Restore that exact revision as a separate rollback operation; do not copy shared `_core`, `nodes`, or `web` folders between versions. diff --git a/docs/nodes/ai-influencer-resolution-2k4k.md b/docs/nodes/ai-influencer-resolution-2k4k.md index 73029cc..87a941d 100644 --- a/docs/nodes/ai-influencer-resolution-2k4k.md +++ b/docs/nodes/ai-influencer-resolution-2k4k.md @@ -1,6 +1,7 @@ # MATRIX AI INFLUENCER RESOLUTION 2K/4K -Class ID: `MATRIXLAB_AIInfluencerResolution2K4K` +Class ID: `MATRIX_AIInfluencerResolution2K4K` + Category: `MATRIX LAB/Resolution & Layout` diff --git a/docs/nodes/ai-influencer-resolution.md b/docs/nodes/ai-influencer-resolution.md index d766094..de097a8 100644 --- a/docs/nodes/ai-influencer-resolution.md +++ b/docs/nodes/ai-influencer-resolution.md @@ -1,6 +1,7 @@ # MATRIX AI INFLUENCER RESOLUTION -Class ID: `MATRIXLAB_AIInfluencerResolution` +Class ID: `MATRIX_AIInfluencerResolution` + Category: `MATRIX LAB/Resolution & Layout` diff --git a/docs/nodes/auto-prompter.md b/docs/nodes/auto-prompter.md index e4218e9..3706066 100644 --- a/docs/nodes/auto-prompter.md +++ b/docs/nodes/auto-prompter.md @@ -1,6 +1,7 @@ # MATRIX AUTO PROMPTER -Class ID: `MATRIXLAB_PromptDirector` +Class ID: `MATRIX_AutoPrompter` + Category: `MATRIX LAB/Prompting` diff --git a/docs/nodes/easy-crop.md b/docs/nodes/easy-crop.md index 35b61bc..cf1b0f8 100644 --- a/docs/nodes/easy-crop.md +++ b/docs/nodes/easy-crop.md @@ -1,6 +1,7 @@ # MATRIX EASY CROP -Class ID: `MATRIXLAB_EasyCrop` +Class ID: `MATRIX_EasyCrop` + Category: `MATRIX LAB/Image Processing` diff --git a/docs/nodes/image-batch-loader.md b/docs/nodes/image-batch-loader.md index 621dd3c..d86e489 100644 --- a/docs/nodes/image-batch-loader.md +++ b/docs/nodes/image-batch-loader.md @@ -1,6 +1,7 @@ # MATRIX IMAGE BATCH LOADER -Class ID: `MATRIXLAB_ImageBatchLoader` +Class ID: `MATRIX_ImageBatchLoader` + Category: `MATRIX LAB/Input & Output` diff --git a/docs/nodes/matrix-save-clean.md b/docs/nodes/metadata-killer.md similarity index 93% rename from docs/nodes/matrix-save-clean.md rename to docs/nodes/metadata-killer.md index 84c3cb2..46f6467 100644 --- a/docs/nodes/matrix-save-clean.md +++ b/docs/nodes/metadata-killer.md @@ -1,6 +1,7 @@ # MATRIX METADATA KILLER -Class ID: `MATRIX_SaveClean` +Class ID: `MATRIX_MetadataKiller` + Category: `MATRIX LAB/Input & Output` diff --git a/docs/nodes/resolution.md b/docs/nodes/resolution.md index 15a7700..6324289 100644 --- a/docs/nodes/resolution.md +++ b/docs/nodes/resolution.md @@ -1,6 +1,7 @@ # MATRIX RESOLUTION -Class ID: `MATRIXLAB_Resolution` +Class ID: `MATRIX_Resolution` + Category: `MATRIX LAB/Resolution & Layout` diff --git a/docs/nodes/spectral-sampler.md b/docs/nodes/spectral-sampler.md index 47a9b87..a077040 100644 --- a/docs/nodes/spectral-sampler.md +++ b/docs/nodes/spectral-sampler.md @@ -1,6 +1,7 @@ # MATRIX SPECTRAL SAMPLER -Class ID: `MATRIXSpectralSampler` +Class ID: `MATRIX_SpectralSampler` + Category: `MATRIX LAB/Sampling & Detail` diff --git a/nodes/image_processing/matrixlab_easycrop.py b/nodes/image_processing/matrix_easycrop.py similarity index 97% rename from nodes/image_processing/matrixlab_easycrop.py rename to nodes/image_processing/matrix_easycrop.py index 2b11ec3..652acdd 100644 --- a/nodes/image_processing/matrixlab_easycrop.py +++ b/nodes/image_processing/matrix_easycrop.py @@ -1,9 +1,9 @@ -"""Compiled declaration for MATRIXLAB_EasyCrop; regenerate instead of hand-editing.""" +"""Compiled declaration for MATRIX_EasyCrop; regenerate instead of hand-editing.""" from __future__ import annotations from ..._core.flow_utility import execute_compiled_node -NODE_ID = 'MATRIXLAB_EasyCrop' +NODE_ID = 'MATRIX_EasyCrop' OPERATION_BLOCK = 'easy.crop' SCHEMA_WIDGETS = { 'image': ('STRING', {'default': '', 'image_upload': True}), @@ -144,7 +144,7 @@ def _payload_items(inputs): for index in range(count) ) -class MATRIXLABEasyCrop: +class MATRIXEasyCrop: @classmethod def INPUT_TYPES(cls): return { @@ -199,5 +199,5 @@ async def execute(self, **inputs): } return await execute_compiled_node(NODE_ID, '', '', inputs) -NODE_CLASS_MAPPINGS = {NODE_ID: MATRIXLABEasyCrop} +NODE_CLASS_MAPPINGS = {NODE_ID: MATRIXEasyCrop} NODE_DISPLAY_NAME_MAPPINGS = {NODE_ID: 'MATRIX EASY CROP'} diff --git a/nodes/input_output/matrixlab_imagebatchloader.py b/nodes/input_output/matrix_imagebatchloader.py similarity index 96% rename from nodes/input_output/matrixlab_imagebatchloader.py rename to nodes/input_output/matrix_imagebatchloader.py index 2092517..9f7d089 100644 --- a/nodes/input_output/matrixlab_imagebatchloader.py +++ b/nodes/input_output/matrix_imagebatchloader.py @@ -1,9 +1,9 @@ -"""Compiled declaration for MATRIXLAB_ImageBatchLoader; regenerate instead of hand-editing.""" +"""Compiled declaration for MATRIX_ImageBatchLoader; regenerate instead of hand-editing.""" from __future__ import annotations from ..._core.flow_utility import execute_compiled_node -NODE_ID = 'MATRIXLAB_ImageBatchLoader' +NODE_ID = 'MATRIX_ImageBatchLoader' OPERATION_BLOCK = 'io.image-collection' SCHEMA_WIDGETS = { 'collection': ('STRING', {'default': '{"version":1,"items":[],"selected":null}'}), @@ -137,7 +137,7 @@ def _payload_items(inputs): for index in range(count) ) -class MATRIXLABImageBatchLoader: +class MATRIXImageBatchLoader: @classmethod def INPUT_TYPES(cls): return { @@ -172,5 +172,5 @@ async def execute(self, **inputs): import asyncio return await asyncio.to_thread(_operation_block.execute_utility_operation, inputs) -NODE_CLASS_MAPPINGS = {NODE_ID: MATRIXLABImageBatchLoader} +NODE_CLASS_MAPPINGS = {NODE_ID: MATRIXImageBatchLoader} NODE_DISPLAY_NAME_MAPPINGS = {NODE_ID: 'MATRIX IMAGE BATCH LOADER'} diff --git a/nodes/input_output/matrix_saveclean.py b/nodes/input_output/matrix_metadatakiller.py similarity index 97% rename from nodes/input_output/matrix_saveclean.py rename to nodes/input_output/matrix_metadatakiller.py index 49ae3b3..20c4f4c 100644 --- a/nodes/input_output/matrix_saveclean.py +++ b/nodes/input_output/matrix_metadatakiller.py @@ -1,9 +1,9 @@ -"""Compiled declaration for MATRIX_SaveClean; regenerate instead of hand-editing.""" +"""Compiled declaration for MATRIX_MetadataKiller; regenerate instead of hand-editing.""" from __future__ import annotations from ..._core.flow_utility import execute_compiled_node -NODE_ID = 'MATRIX_SaveClean' +NODE_ID = 'MATRIX_MetadataKiller' OPERATION_BLOCK = 'io.save-clean' SCHEMA_WIDGETS = { 'images': ('IMAGE', {'forceInput': True}), @@ -140,7 +140,7 @@ def _payload_items(inputs): for index in range(count) ) -class MATRIXSaveClean: +class MATRIXMetadataKiller: @classmethod def INPUT_TYPES(cls): return { @@ -168,5 +168,5 @@ async def execute(self, **inputs): from ..._core import io_save_clean as _operation_block return _operation_block.execute_utility_operation(inputs) -NODE_CLASS_MAPPINGS = {NODE_ID: MATRIXSaveClean} +NODE_CLASS_MAPPINGS = {NODE_ID: MATRIXMetadataKiller} NODE_DISPLAY_NAME_MAPPINGS = {NODE_ID: 'MATRIX METADATA KILLER'} diff --git a/nodes/prompting/matrix_autoprompter.py b/nodes/prompting/matrix_autoprompter.py new file mode 100644 index 0000000..cac2fd0 --- /dev/null +++ b/nodes/prompting/matrix_autoprompter.py @@ -0,0 +1,6 @@ +"""Prompting registration; shared service implementation stays in the core.""" +from ..._core.prompt_director import MATRIX_AutoPrompter as _PromptDirector + + +class MATRIX_AutoPrompter(_PromptDirector): + CATEGORY = 'MATRIX LAB/Prompting' diff --git a/nodes/prompting/matrixlab_promptdirector.py b/nodes/prompting/matrixlab_promptdirector.py deleted file mode 100644 index 117c40e..0000000 --- a/nodes/prompting/matrixlab_promptdirector.py +++ /dev/null @@ -1,6 +0,0 @@ -"""Prompting registration; shared service implementation stays in the core.""" -from ..._core.prompt_director import MATRIXLAB_PromptDirector as _PromptDirector - - -class MATRIXLAB_PromptDirector(_PromptDirector): - CATEGORY = 'MATRIX LAB/Prompting' diff --git a/nodes/resolution_layout/matrixlab_aiinfluencerresolution.py b/nodes/resolution_layout/matrix_aiinfluencerresolution.py similarity index 96% rename from nodes/resolution_layout/matrixlab_aiinfluencerresolution.py rename to nodes/resolution_layout/matrix_aiinfluencerresolution.py index 8a79391..8e7287d 100644 --- a/nodes/resolution_layout/matrixlab_aiinfluencerresolution.py +++ b/nodes/resolution_layout/matrix_aiinfluencerresolution.py @@ -1,10 +1,10 @@ -"""Compiled declaration for MATRIXLAB_AIInfluencerResolution; regenerate instead of hand-editing.""" +"""Compiled declaration for MATRIX_AIInfluencerResolution; regenerate instead of hand-editing.""" from __future__ import annotations from ..._core import resolution_ai_influencer as _operation_block from ..._core.flow_utility import execute_compiled_node -NODE_ID = 'MATRIXLAB_AIInfluencerResolution' +NODE_ID = 'MATRIX_AIInfluencerResolution' OPERATION_BLOCK = 'resolution.ai-influencer' SCHEMA_WIDGETS = { 'aspect_ratio': (['1:1', '9:16', '3:4'], {'default': '3:4', 'tooltip': 'AI-influencer delivery aspect ratio.'}), @@ -139,7 +139,7 @@ def _payload_items(inputs): for index in range(count) ) -class MATRIXLABAIInfluencerResolution: +class MATRIXAIInfluencerResolution: @classmethod def INPUT_TYPES(cls): return { @@ -173,5 +173,5 @@ async def execute(self, **inputs): } return await execute_compiled_node(NODE_ID, '', '', inputs) -NODE_CLASS_MAPPINGS = {NODE_ID: MATRIXLABAIInfluencerResolution} +NODE_CLASS_MAPPINGS = {NODE_ID: MATRIXAIInfluencerResolution} NODE_DISPLAY_NAME_MAPPINGS = {NODE_ID: 'MATRIX AI INFLUENCER RESOLUTION'} diff --git a/nodes/resolution_layout/matrixlab_aiinfluencerresolution2k4k.py b/nodes/resolution_layout/matrix_aiinfluencerresolution2k4k.py similarity index 96% rename from nodes/resolution_layout/matrixlab_aiinfluencerresolution2k4k.py rename to nodes/resolution_layout/matrix_aiinfluencerresolution2k4k.py index 70cbd99..e96b257 100644 --- a/nodes/resolution_layout/matrixlab_aiinfluencerresolution2k4k.py +++ b/nodes/resolution_layout/matrix_aiinfluencerresolution2k4k.py @@ -1,10 +1,10 @@ -"""Compiled declaration for MATRIXLAB_AIInfluencerResolution2K4K; regenerate instead of hand-editing.""" +"""Compiled declaration for MATRIX_AIInfluencerResolution2K4K; regenerate instead of hand-editing.""" from __future__ import annotations from ..._core import resolution_ai_influencer_2k4k as _operation_block from ..._core.flow_utility import execute_compiled_node -NODE_ID = 'MATRIXLAB_AIInfluencerResolution2K4K' +NODE_ID = 'MATRIX_AIInfluencerResolution2K4K' OPERATION_BLOCK = 'resolution.ai-influencer-2k4k' SCHEMA_WIDGETS = { 'aspect_ratio': (['1:1', '9:16', '3:4'], {'default': '3:4', 'tooltip': 'AI-influencer delivery aspect ratio.'}), @@ -139,7 +139,7 @@ def _payload_items(inputs): for index in range(count) ) -class MATRIXLABAIInfluencerResolution2K4K: +class MATRIXAIInfluencerResolution2K4K: @classmethod def INPUT_TYPES(cls): return { @@ -173,5 +173,5 @@ async def execute(self, **inputs): } return await execute_compiled_node(NODE_ID, '', '', inputs) -NODE_CLASS_MAPPINGS = {NODE_ID: MATRIXLABAIInfluencerResolution2K4K} +NODE_CLASS_MAPPINGS = {NODE_ID: MATRIXAIInfluencerResolution2K4K} NODE_DISPLAY_NAME_MAPPINGS = {NODE_ID: 'MATRIX AI INFLUENCER RESOLUTION 2K/4K'} diff --git a/nodes/resolution_layout/matrixlab_resolution.py b/nodes/resolution_layout/matrix_resolution.py similarity index 97% rename from nodes/resolution_layout/matrixlab_resolution.py rename to nodes/resolution_layout/matrix_resolution.py index d93e83f..a79790a 100644 --- a/nodes/resolution_layout/matrixlab_resolution.py +++ b/nodes/resolution_layout/matrix_resolution.py @@ -1,10 +1,10 @@ -"""Compiled declaration for MATRIXLAB_Resolution; regenerate instead of hand-editing.""" +"""Compiled declaration for MATRIX_Resolution; regenerate instead of hand-editing.""" from __future__ import annotations from ..._core import resolution_dimensions as _operation_block from ..._core.flow_utility import execute_compiled_node -NODE_ID = 'MATRIXLAB_Resolution' +NODE_ID = 'MATRIX_Resolution' OPERATION_BLOCK = 'resolution.dimensions' SCHEMA_WIDGETS = { 'aspect_ratio': (['1:1', '16:9', '9:16', '4:3', '3:4', '3:2', '2:3', '4:5', 'Custom'], {'default': '3:4', 'tooltip': 'Preset aspect ratio, or exact custom pixel dimensions.'}), @@ -141,7 +141,7 @@ def _payload_items(inputs): for index in range(count) ) -class MATRIXLABResolution: +class MATRIXResolution: @classmethod def INPUT_TYPES(cls): return { @@ -177,5 +177,5 @@ async def execute(self, **inputs): } return await execute_compiled_node(NODE_ID, '', '', inputs) -NODE_CLASS_MAPPINGS = {NODE_ID: MATRIXLABResolution} +NODE_CLASS_MAPPINGS = {NODE_ID: MATRIXResolution} NODE_DISPLAY_NAME_MAPPINGS = {NODE_ID: 'MATRIX RESOLUTION'} diff --git a/nodes/sampling_detail/matrixspectralsampler.py b/nodes/sampling_detail/matrix_spectralsampler.py similarity index 98% rename from nodes/sampling_detail/matrixspectralsampler.py rename to nodes/sampling_detail/matrix_spectralsampler.py index f550d17..418167b 100644 --- a/nodes/sampling_detail/matrixspectralsampler.py +++ b/nodes/sampling_detail/matrix_spectralsampler.py @@ -1,9 +1,9 @@ -"""Compiled declaration for MATRIXSpectralSampler; regenerate instead of hand-editing.""" +"""Compiled declaration for MATRIX_SpectralSampler; regenerate instead of hand-editing.""" from __future__ import annotations from ..._core.flow_utility import execute_compiled_node -NODE_ID = 'MATRIXSpectralSampler' +NODE_ID = 'MATRIX_SpectralSampler' OPERATION_BLOCK = 'sample.spectral-hires' SCHEMA_WIDGETS = { 'base_sampler': (['euler', 'euler_cfg_pp', 'euler_ancestral', 'euler_ancestral_cfg_pp', 'heun', 'heunpp2', 'exp_heun_2_x0', 'exp_heun_2_x0_sde', 'dpm_2', 'dpm_2_ancestral', 'lms', 'dpm_fast', 'dpm_adaptive', 'dpmpp_2s_ancestral', 'dpmpp_2s_ancestral_cfg_pp', 'dpmpp_sde', 'dpmpp_sde_gpu', 'dpmpp_2m', 'dpmpp_2m_cfg_pp', 'dpmpp_2m_sde', 'dpmpp_2m_sde_gpu', 'dpmpp_2m_sde_heun', 'dpmpp_2m_sde_heun_gpu', 'dpmpp_3m_sde', 'dpmpp_3m_sde_gpu', 'ddpm', 'lcm', 'ipndm', 'ipndm_v', 'deis', 'res_multistep', 'res_multistep_cfg_pp', 'res_multistep_ancestral', 'res_multistep_ancestral_cfg_pp', 'gradient_estimation', 'gradient_estimation_cfg_pp', 'er_sde', 'seeds_2', 'seeds_3', 'sa_solver', 'sa_solver_pece', 'ddim', 'uni_pc', 'uni_pc_bh2'], {'default': 'euler'}), diff --git a/pyproject.toml b/pyproject.toml index 92f6354..e7a1b91 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "matrix-lab-nodes" -version = "0.2.0" +version = "0.3.0" description = "MATRIX LAB custom nodes for ComfyUI." readme = "README.md" requires-python = ">=3.10" diff --git a/tests/test_distribution.py b/tests/test_distribution.py index fec761b..9b0c983 100644 --- a/tests/test_distribution.py +++ b/tests/test_distribution.py @@ -25,21 +25,21 @@ PACKAGE_NAME = "matrixlab_public_distribution_under_test" RETIRED_NODE_IDS = {"MATRIX_CameraLook", "MATRIX_Renoise"} BASE_NODE_IDS = { - "MATRIXLAB_AIInfluencerResolution", - "MATRIXLAB_EasyCrop", - "MATRIXLAB_ImageBatchLoader", - "MATRIXLAB_PromptDirector", - "MATRIXLAB_Resolution", - "MATRIXSpectralSampler", + "MATRIX_AIInfluencerResolution", + "MATRIX_EasyCrop", + "MATRIX_ImageBatchLoader", + "MATRIX_AutoPrompter", + "MATRIX_Resolution", + "MATRIX_SpectralSampler", "MATRIX_CropTailPaste", "MATRIX_EyeMask", "MATRIX_LatentTail", "MATRIX_OutputStage", "MATRIX_PhotoFinisher", - "MATRIX_SaveClean", + "MATRIX_MetadataKiller", "MATRIX_SkinMask", } -ADDITIVE_NODE_ID = "MATRIXLAB_AIInfluencerResolution2K4K" +ADDITIVE_NODE_ID = "MATRIX_AIInfluencerResolution2K4K" NODE_DIRECTORIES = { "image_processing", "input_output", @@ -105,6 +105,14 @@ def test_package_and_manifest_versions_agree(self): self.assertIsNotNone(match, "pyproject.toml must declare a project version") self.assertEqual(match.group(1), self.manifest["version"]) + def test_class_ids_match_public_product_names(self): + for node_id, display in self.pack.NODE_DISPLAY_NAME_MAPPINGS.items(): + with self.subTest(node_id=node_id): + self.assertRegex(node_id, r"^MATRIX_[A-Z][A-Za-z0-9]*$") + self.assertTrue(display.startswith("MATRIX ")) + self.assertEqual(node_id[len("MATRIX_"):].upper(), + re.sub(r"[^A-Z0-9]", "", display[len("MATRIX "):])) + def test_distribution_has_exactly_the_six_public_node_directories(self): actual = { path.name @@ -252,7 +260,7 @@ def setUpClass(cls): cls.pack = _load_pack() def test_prompt_director_manual_execution_returns_saved_text(self): - node = self.pack.NODE_CLASS_MAPPINGS["MATRIXLAB_PromptDirector"]() + node = self.pack.NODE_CLASS_MAPPINGS["MATRIX_AutoPrompter"]() saved = " a manually edited prompt; no provider call " with mock.patch.object(socket.socket, "connect", side_effect=AssertionError("network attempted")): self.assertEqual(node.execute(final_prompt=saved), (saved,)) diff --git a/tests/test_identity_migration.py b/tests/test_identity_migration.py new file mode 100644 index 0000000..eb25052 --- /dev/null +++ b/tests/test_identity_migration.py @@ -0,0 +1,168 @@ +"""Tests for the standalone workflow identity migration tool.""" +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest +from unittest import mock + + +ROOT = Path(__file__).resolve().parents[1] +TOOL_PATH = ROOT / "tools" / "migrate_workflow.py" +SPEC = importlib.util.spec_from_file_location("matrixlab_migrate_workflow", TOOL_PATH) +if SPEC is None or SPEC.loader is None: + raise RuntimeError("could not load migration tool") +MIGRATION = importlib.util.module_from_spec(SPEC) +sys.modules[SPEC.name] = MIGRATION +SPEC.loader.exec_module(MIGRATION) + + +class IdentityMigrationTests(unittest.TestCase): + def test_mapping_covers_the_eight_reviewed_identity_changes(self): + self.assertEqual( + MIGRATION.IDENTITY_MIGRATIONS, + { + "MATRIX_SaveClean": "MATRIX_MetadataKiller", + "MATRIXLAB_PromptDirector": "MATRIX_AutoPrompter", + "MATRIXLAB_ImageBatchLoader": "MATRIX_ImageBatchLoader", + "MATRIXLAB_Resolution": "MATRIX_Resolution", + "MATRIXLAB_AIInfluencerResolution": "MATRIX_AIInfluencerResolution", + "MATRIXLAB_AIInfluencerResolution2K4K": "MATRIX_AIInfluencerResolution2K4K", + "MATRIXLAB_EasyCrop": "MATRIX_EasyCrop", + "MATRIXSpectralSampler": "MATRIX_SpectralSampler", + }, + ) + + def test_ui_migration_is_pure_narrow_and_idempotent(self): + source = { + "nodes": [ + { + "id": 1, + "type": "MATRIX_SaveClean", + "properties": { + "Node name for S&R": "MATRIX_SaveClean", + "note": "MATRIX_SaveClean must remain here", + }, + "widgets_values": ["MATRIX_SaveClean", 100], + }, + { + "id": 2, + "type": "MATRIXLAB_PromptDirector", + "properties": {"Node name for S&R": "custom-alias"}, + }, + {"id": 3, "type": "MATRIX_Renoise"}, + ], + "extra": {"prompt": "MATRIXLAB_PromptDirector"}, + "links": [[4, 1, 0, 2, 0, "IMAGE"]], + } + + migrated, report = MIGRATION.migrate_workflow(source) + + self.assertEqual(source["nodes"][0]["type"], "MATRIX_SaveClean") + self.assertEqual(migrated["nodes"][0]["type"], "MATRIX_MetadataKiller") + self.assertEqual( + migrated["nodes"][0]["properties"]["Node name for S&R"], + "MATRIX_MetadataKiller", + ) + self.assertEqual(migrated["nodes"][0]["properties"]["note"], + "MATRIX_SaveClean must remain here") + self.assertEqual(migrated["nodes"][0]["widgets_values"], ["MATRIX_SaveClean", 100]) + self.assertEqual(migrated["extra"], source["extra"]) + self.assertEqual(migrated["links"], source["links"]) + self.assertEqual(migrated["nodes"][1]["properties"]["Node name for S&R"], "custom-alias") + self.assertEqual(report.migrated_nodes, + {"MATRIX_SaveClean": 1, "MATRIXLAB_PromptDirector": 1}) + self.assertEqual(report.replaced_identity_fields, 3) + self.assertEqual(report.retired_nodes, {"MATRIX_Renoise": 1}) + + repeated, repeated_report = MIGRATION.migrate_workflow(migrated) + self.assertEqual(repeated, migrated) + self.assertEqual(repeated_report.migrated_nodes, {}) + self.assertEqual(repeated_report.replaced_identity_fields, 0) + self.assertEqual(repeated_report.retired_nodes, {"MATRIX_Renoise": 1}) + + def test_direct_and_enveloped_api_prompts_only_change_class_type(self): + prompt = { + str(index): { + "class_type": old, + "inputs": {"text": old}, + "_meta": {"title": old}, + } + for index, old in enumerate(MIGRATION.IDENTITY_MIGRATIONS, start=1) + } + prompt["retired"] = {"class_type": "MATRIX_CameraLook", "inputs": {}} + for source in (prompt, {"prompt": prompt, "client_id": "MATRIX_SaveClean"}): + with self.subTest(enveloped="prompt" in source): + migrated, report = MIGRATION.migrate_workflow(source) + migrated_prompt = migrated.get("prompt", migrated) + for index, (old, new) in enumerate(MIGRATION.IDENTITY_MIGRATIONS.items(), start=1): + with self.subTest(node_id=index): + node = migrated_prompt[str(index)] + self.assertEqual(node["class_type"], new) + self.assertEqual(node["inputs"], {"text": old}) + self.assertEqual(node["_meta"], {"title": old}) + if "prompt" in source: + self.assertEqual(migrated["client_id"], "MATRIX_SaveClean") + self.assertEqual(report.format, "api") + self.assertEqual(report.retired_nodes, {"MATRIX_CameraLook": 1}) + + def test_malformed_identity_fields_raise_clear_value_errors(self): + for source, message in ( + ({"nodes": [{"type": ["MATRIX_SaveClean"]}]}, "UI node.type must be a string"), + ({"1": {"class_type": ["MATRIX_SaveClean"]}}, + "API node.class_type must be a string"), + ): + with self.subTest(message=message), self.assertRaisesRegex(ValueError, message): + MIGRATION.migrate_workflow(source) + + def test_cli_writes_new_output_and_refuses_overwrites(self): + source_document = {"1": {"class_type": "MATRIXSpectralSampler", "inputs": {}}} + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + source = root / "source.json" + output = root / "output.json" + source.write_text(json.dumps(source_document), encoding="utf-8") + + completed = subprocess.run( + [sys.executable, str(TOOL_PATH), "--input", str(source), "--output", str(output)], + check=False, + capture_output=True, + text=True, + ) + self.assertEqual(completed.returncode, 0, completed.stderr) + self.assertEqual(json.loads(output.read_text(encoding="utf-8"))["1"]["class_type"], + "MATRIX_SpectralSampler") + original = source.read_bytes() + + existing = subprocess.run( + [sys.executable, str(TOOL_PATH), "--input", str(source), "--output", str(output)], + check=False, + capture_output=True, + text=True, + ) + self.assertNotEqual(existing.returncode, 0) + self.assertIn("refusing existing output", existing.stderr) + + with mock.patch.object(Path, "exists", return_value=False): + with self.assertRaisesRegex(SystemExit, "refusing existing output"): + MIGRATION.main(["--input", str(source), "--output", str(output)]) + self.assertEqual(json.loads(output.read_text(encoding="utf-8"))["1"]["class_type"], + "MATRIX_SpectralSampler") + + same = subprocess.run( + [sys.executable, str(TOOL_PATH), "--input", str(source), "--output", str(source)], + check=False, + capture_output=True, + text=True, + ) + self.assertNotEqual(same.returncode, 0) + self.assertIn("refusing to overwrite", same.stderr) + self.assertEqual(source.read_bytes(), original) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/migrate_workflow.py b/tools/migrate_workflow.py new file mode 100644 index 0000000..933f722 --- /dev/null +++ b/tools/migrate_workflow.py @@ -0,0 +1,148 @@ +"""Migrate saved ComfyUI workflows to MATRIX LAB's normalized node IDs. + +The migration is deliberately narrow: it changes registered class identities in +UI or API graphs and leaves every input, widget, link, prompt, and other string +untouched. Retired finish nodes are reported because replacing them requires a +separate semantic decision. +""" +from __future__ import annotations + +import argparse +from collections import Counter +from copy import deepcopy +from dataclasses import asdict, dataclass +import json +from pathlib import Path +from typing import Any, Mapping + + +IDENTITY_MIGRATIONS = { + "MATRIX_SaveClean": "MATRIX_MetadataKiller", + "MATRIXLAB_PromptDirector": "MATRIX_AutoPrompter", + "MATRIXLAB_ImageBatchLoader": "MATRIX_ImageBatchLoader", + "MATRIXLAB_Resolution": "MATRIX_Resolution", + "MATRIXLAB_AIInfluencerResolution": "MATRIX_AIInfluencerResolution", + "MATRIXLAB_AIInfluencerResolution2K4K": "MATRIX_AIInfluencerResolution2K4K", + "MATRIXLAB_EasyCrop": "MATRIX_EasyCrop", + "MATRIXSpectralSampler": "MATRIX_SpectralSampler", +} +RETIRED_NODE_IDS = frozenset({"MATRIX_CameraLook", "MATRIX_Renoise"}) + + +@dataclass(frozen=True) +class MigrationReport: + format: str + migrated_nodes: Mapping[str, int] + replaced_identity_fields: int + retired_nodes: Mapping[str, int] + + +def _record_identity(value: Any, counts: Counter[str]) -> Any: + if isinstance(value, str) and value in IDENTITY_MIGRATIONS: + counts[value] += 1 + return IDENTITY_MIGRATIONS[value] + return value + + +def _identity(value: Any, location: str) -> str | None: + if value is None or isinstance(value, str): + return value + raise ValueError(f"malformed workflow: {location} must be a string") + + +def _report(format_name: str, migrated: Counter[str], fields: int, + retired: Counter[str]) -> MigrationReport: + return MigrationReport( + format=format_name, + migrated_nodes={key: migrated[key] for key in IDENTITY_MIGRATIONS if migrated[key]}, + replaced_identity_fields=fields, + retired_nodes={key: retired[key] for key in sorted(RETIRED_NODE_IDS) if retired[key]}, + ) + + +def migrate_workflow(document: Any) -> tuple[Any, MigrationReport]: + """Return a migrated deep copy and a deterministic migration report. + + Supported shapes are a UI graph with a top-level ``nodes`` list, a direct + API prompt mapping, and the common ``{"prompt": }`` envelope. + """ + result = deepcopy(document) + migrated: Counter[str] = Counter() + retired: Counter[str] = Counter() + replaced_fields = 0 + + if isinstance(result, dict) and isinstance(result.get("nodes"), list): + for node in result["nodes"]: + if not isinstance(node, dict): + continue + node_type = _identity(node.get("type"), "UI node.type") + if node_type in RETIRED_NODE_IDS: + retired[node_type] += 1 + replacement = _record_identity(node_type, migrated) + if replacement != node_type: + node["type"] = replacement + replaced_fields += 1 + + properties = node.get("properties") + if isinstance(properties, dict) and "Node name for S&R" in properties: + search_name = properties["Node name for S&R"] + replacement = _record_identity(search_name, Counter()) + if replacement != search_name: + properties["Node name for S&R"] = replacement + replaced_fields += 1 + return result, _report("ui", migrated, replaced_fields, retired) + + api = result.get("prompt") if isinstance(result, dict) and "prompt" in result else result + if not isinstance(api, dict): + raise ValueError("unsupported workflow JSON: expected a UI graph or API prompt mapping") + + for node in api.values(): + if not isinstance(node, dict): + continue + class_type = _identity(node.get("class_type"), "API node.class_type") + if class_type in RETIRED_NODE_IDS: + retired[class_type] += 1 + replacement = _record_identity(class_type, migrated) + if replacement != class_type: + node["class_type"] = replacement + replaced_fields += 1 + return result, _report("api", migrated, replaced_fields, retired) + + +def _parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--input", required=True, type=Path, help="source UI or API workflow JSON") + parser.add_argument("--output", required=True, type=Path, help="new migrated workflow JSON") + return parser + + +def main(argv: list[str] | None = None) -> int: + args = _parser().parse_args(argv) + source = args.input.resolve() + output = args.output.resolve() + if source == output: + raise SystemExit("refusing to overwrite the input workflow") + if output.exists(): + raise SystemExit(f"refusing existing output: {output}") + if not source.is_file(): + raise SystemExit(f"input workflow does not exist: {source}") + + try: + document = json.loads(source.read_text(encoding="utf-8")) + migrated, report = migrate_workflow(document) + except (OSError, UnicodeError, json.JSONDecodeError, ValueError) as exc: + raise SystemExit(f"could not migrate workflow: {exc}") from exc + + output.parent.mkdir(parents=True, exist_ok=True) + try: + with output.open("x", encoding="utf-8", newline="\n") as handle: + json.dump(migrated, handle, indent=2, ensure_ascii=False) + handle.write("\n") + except FileExistsError as exc: + raise SystemExit(f"refusing existing output: {output}") from exc + print(json.dumps(asdict(report), sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/web/ai_influencer_resolution.bd473fd081a9ee32.js b/web/ai_influencer_resolution.d47e945783aa6298.js similarity index 98% rename from web/ai_influencer_resolution.bd473fd081a9ee32.js rename to web/ai_influencer_resolution.d47e945783aa6298.js index 4837df8..dd927f2 100644 --- a/web/ai_influencer_resolution.bd473fd081a9ee32.js +++ b/web/ai_influencer_resolution.d47e945783aa6298.js @@ -1,5 +1,5 @@ import { app } from "../../scripts/app.js"; -import { createHaloResolutionDeck, createHaloWidgetHost, haloMinimumNodeHeight, haloWidgetLayoutHeight, measureHaloContentHeight, measureHaloHorizontalChrome, setHaloNodeSize, calculateTierResolution, migrateResolutionGraph } from "./halo_resolution.3078c6fb7c5009a2.mjs"; +import { createHaloResolutionDeck, createHaloWidgetHost, haloMinimumNodeHeight, haloWidgetLayoutHeight, measureHaloContentHeight, measureHaloHorizontalChrome, setHaloNodeSize, calculateTierResolution, migrateResolutionGraph } from "./halo_resolution.83fe73faf4ac1a41.mjs"; const ASPECT_RATIOS = ["1:1", "9:16", "3:4"]; const RESOLUTION_TIERS = ["1K", "2K", "4K"]; export function calculateResolution(state) { @@ -8,7 +8,7 @@ export function calculateResolution(state) { } const NODE_IDS = new Set( - "MATRIXLAB_AIInfluencerResolution".split(",").filter(Boolean), + "MATRIX_AIInfluencerResolution".split(",").filter(Boolean), ); const DEFAULT_STATE = Object.freeze({ aspect_ratio: "3:4", resolution_tier: "2K" }); const MIN_SURFACE_WIDTH = 420; diff --git a/web/ai_influencer_resolution_2k4k.5a6f0a9d0be56983.js b/web/ai_influencer_resolution_2k4k.203f0bdb857d180d.js similarity index 98% rename from web/ai_influencer_resolution_2k4k.5a6f0a9d0be56983.js rename to web/ai_influencer_resolution_2k4k.203f0bdb857d180d.js index a439d3a..f7cc734 100644 --- a/web/ai_influencer_resolution_2k4k.5a6f0a9d0be56983.js +++ b/web/ai_influencer_resolution_2k4k.203f0bdb857d180d.js @@ -1,5 +1,5 @@ import { app } from "../../scripts/app.js"; -import { createHaloResolutionDeck, createHaloWidgetHost, haloMinimumNodeHeight, haloWidgetLayoutHeight, measureHaloContentHeight, measureHaloHorizontalChrome, setHaloNodeSize, calculateTierResolution } from "./halo_resolution.3078c6fb7c5009a2.mjs"; +import { createHaloResolutionDeck, createHaloWidgetHost, haloMinimumNodeHeight, haloWidgetLayoutHeight, measureHaloContentHeight, measureHaloHorizontalChrome, setHaloNodeSize, calculateTierResolution } from "./halo_resolution.83fe73faf4ac1a41.mjs"; const ASPECT_RATIOS = ["1:1", "9:16", "3:4"]; const RESOLUTION_TIERS = ["2K", "4K"]; export function calculateResolution(state) { @@ -8,7 +8,7 @@ export function calculateResolution(state) { } const NODE_IDS = new Set( - "MATRIXLAB_AIInfluencerResolution2K4K".split(",").filter(Boolean), + "MATRIX_AIInfluencerResolution2K4K".split(",").filter(Boolean), ); const DEFAULT_STATE = Object.freeze({ aspect_ratio: "3:4", resolution_tier: "2K" }); const MIN_SURFACE_WIDTH = 420; diff --git a/web/easy_crop.e448d80a597cbf32.js b/web/easy_crop.4fae2e79d5ac41d4.js similarity index 98% rename from web/easy_crop.e448d80a597cbf32.js rename to web/easy_crop.4fae2e79d5ac41d4.js index ee875a4..f1905fe 100644 --- a/web/easy_crop.e448d80a597cbf32.js +++ b/web/easy_crop.4fae2e79d5ac41d4.js @@ -1,8 +1,8 @@ import { app } from "../../scripts/app.js"; import { api } from "../../scripts/api.js"; -import { createHaloWidgetHost, haloMinimumNodeHeight, haloWidgetLayoutHeight, measureHaloContentHeight, measureHaloHorizontalChrome, mountHaloSurface, setHaloNodeSize } from "./halo.3bc35993e091e5c5.mjs"; +import { createHaloWidgetHost, haloMinimumNodeHeight, haloWidgetLayoutHeight, measureHaloContentHeight, measureHaloHorizontalChrome, mountHaloSurface, setHaloNodeSize } from "./halo.d64a9a13ccfeb9ce.mjs"; -const NODE_IDS = new Set(["MATRIXLAB_EasyCrop"]); +const NODE_IDS = new Set(["MATRIX_EasyCrop"]); export const CONTROL = Symbol.for("matrixlab.easy-crop.control"); export const ASPECTS = Object.freeze(["Free", "1:1", "16:9", "9:16", "Custom"]); export const DEFAULT_STATE = Object.freeze({ @@ -160,7 +160,7 @@ function writeState(node, control, next) { try { widget.callback?.call(widget, value, app.canvas, node, undefined, undefined); } catch (error) { - console.warn("MATRIXLAB Easy Crop widget callback failed", error); + console.warn("MATRIX Easy Crop widget callback failed", error); } } control.render?.(); @@ -276,7 +276,7 @@ export function imageViewURL(value) { function createRoot(node, control) { const root = document.createElement("div"); root.setAttribute("role", "group"); - root.setAttribute("aria-label", "MATRIXLAB Easy Crop controls"); + root.setAttribute("aria-label", "MATRIX Easy Crop controls"); Object.assign(root.style, { boxSizing: "border-box", width: "100%", minWidth: "0", padding: "18px 16px", display: "grid", gridAutoRows: "max-content", alignContent: "start", gap: "12px", @@ -653,7 +653,7 @@ function install(node) { try { root = createRoot(node, control); const host = createHaloWidgetHost(root, document); - if (!host) throw new Error("MATRIXLAB Easy Crop HALO host unavailable"); + if (!host) throw new Error("MATRIX Easy Crop HALO host unavailable"); control.host = host; const presentation = node.addDOMWidget(CORE_PREVIEW_WIDGET, "matrixlab-easy-crop", host, {serialize: false, hideOnZoom: false}); if (!presentation) throw new Error("ComfyUI did not create the DOM widget"); @@ -668,7 +668,7 @@ function install(node) { }; control.presentation = presentation; control.halo = mountHaloSurface(root, node, {profile: "ui", app}); - if (!control.halo) throw new Error("MATRIXLAB Easy Crop HALO surface unavailable"); + if (!control.halo) throw new Error("MATRIX Easy Crop HALO surface unavailable"); claimNativeImagePreview(node, control); hidePresentationWidgets(node, control); node[CONTROL] = control; @@ -682,7 +682,7 @@ function install(node) { if (!widgetsBeforeMount.has(node.widgets[index])) node.widgets.splice(index, 1); } control.host?.remove(); - console.warn("MATRIXLAB Easy Crop UI unavailable; native widgets remain active", error); + console.warn("MATRIX Easy Crop UI unavailable; native widgets remain active", error); return null; } diff --git a/web/gallery.e7ba3b92bd70c1e5.mjs b/web/gallery.1b81fe83387a60e1.mjs similarity index 99% rename from web/gallery.e7ba3b92bd70c1e5.mjs rename to web/gallery.1b81fe83387a60e1.mjs index cda54e2..cbffce1 100644 --- a/web/gallery.e7ba3b92bd70c1e5.mjs +++ b/web/gallery.1b81fe83387a60e1.mjs @@ -1,4 +1,4 @@ -import { haloMinimumNodeHeight, measureHaloContentHeight, mountHaloSurface, setHaloNodeSize } from "./halo.3bc35993e091e5c5.mjs"; +import { haloMinimumNodeHeight, measureHaloContentHeight, mountHaloSurface, setHaloNodeSize } from "./halo.d64a9a13ccfeb9ce.mjs"; export const GALLERY_STATE_VERSION = 1; export const MAX_GALLERY_IMAGES = 10; @@ -10,7 +10,7 @@ const MAX_GRID_HEIGHT = 320; const IMAGE_EXTENSIONS = new Set(["png", "jpg", "jpeg"]); function refuse(message) { - throw new Error(`MATRIXLAB image collection: ${message}`); + throw new Error(`MATRIX image collection: ${message}`); } function rejectDuplicateFields(text) { @@ -97,7 +97,7 @@ export function parseGalleryState(value) { rejectDuplicateFields(value); data = JSON.parse(value); } catch (error) { - if (error instanceof Error && error.message.startsWith("MATRIXLAB image collection:")) throw error; + if (error instanceof Error && error.message.startsWith("MATRIX image collection:")) throw error; refuse("state is not valid JSON"); } if (!data || Array.isArray(data) || typeof data !== "object") refuse("state must be an object"); diff --git a/web/halo.3bc35993e091e5c5.mjs b/web/halo.d64a9a13ccfeb9ce.mjs similarity index 99% rename from web/halo.3bc35993e091e5c5.mjs rename to web/halo.d64a9a13ccfeb9ce.mjs index 448ab33..1ec62c8 100644 --- a/web/halo.3bc35993e091e5c5.mjs +++ b/web/halo.d64a9a13ccfeb9ce.mjs @@ -5,12 +5,12 @@ export const HALO_GLYPHS = "アカサタナハマヤラワイキシチニヒミリヰウクスツヌフムユルエケセテネヘメレヱオコソトノホモヨロヲ0123456789"; export const HALO_EXECUTION_NODE_IDS = Object.freeze([ - "MATRIXSpectralSampler", + "MATRIX_SpectralSampler", "MATRIX_LatentTail", "MATRIX_SkinMask", "MATRIX_EyeMask", "MATRIX_CropTailPaste", - "MATRIX_SaveClean", + "MATRIX_MetadataKiller", "MATRIX_OutputStage", "MATRIX_PhotoFinisher", ]); @@ -1111,7 +1111,7 @@ function parameterFieldPresentation(node, widget) { : widget.name === "feather_px" ? "Feather radius in working-resolution pixels. Set zero for exact original-mask paste in Soft mask mode." : "" }; } - if ((node.comfyClass || node.type) !== "MATRIXSpectralSampler") return null; + if ((node.comfyClass || node.type) !== "MATRIX_SpectralSampler") return null; const value = (name) => node.widgets?.find((item) => item.name === name)?.value; const known = (name) => !linkedInput(node, name); const scales = String(value("scales") ?? "").split(",").map((part) => Number(part.trim())); @@ -1387,7 +1387,7 @@ function mountHaloPrimitiveNode(node, options = {}, allowedIds = EXECUTION_IDS, const presentationName = `${presentationBaseName}_${allocateControlId(doc)}`; const fields = canonicalWidgets.filter((widget) => widget?.name).map((widget) => createWidgetField(doc, node, widget, options)); for (const field of fields) root.appendChild(field.field); - const savedImages = nodeType === "MATRIX_SaveClean" ? mountHaloSavedImages(node, root, options) : null; + const savedImages = nodeType === "MATRIX_MetadataKiller" ? mountHaloSavedImages(node, root, options) : null; if (savedImages) Object.assign(root.style, { flex: "1 1 auto", minHeight: "0" }); const contentMinimum = () => savedImages ? Math.max(fields.length * 46 + 36, ...fields.map(({ field }) => (Number(field.offsetTop) || 0) + (Number(field.offsetHeight) || 0) + 18)) + 188 diff --git a/web/halo_execution.615447e8edacbd1a.js b/web/halo_execution.615447e8edacbd1a.js new file mode 100644 index 0000000..9abca62 --- /dev/null +++ b/web/halo_execution.615447e8edacbd1a.js @@ -0,0 +1,10 @@ +import { app } from "../../scripts/app.js"; +import { mountHaloExecutionNode } from "./halo.d64a9a13ccfeb9ce.mjs"; +const ids = new Set(["MATRIX_CropTailPaste", "MATRIX_EyeMask", "MATRIX_LatentTail", "MATRIX_MetadataKiller", "MATRIX_OutputStage", "MATRIX_PhotoFinisher", "MATRIX_SkinMask", "MATRIX_SpectralSampler"]); +const attach = (node) => { + if (ids.has(node.comfyClass || node.type)) mountHaloExecutionNode(node, {app}); +}; +app.registerExtension({ + name: "matrixlab.halo.execution.cc9ad13f358e89d9", + nodeCreated: attach, loadedGraphNode: attach, +}); diff --git a/web/halo_execution.9c05a384226d4180.js b/web/halo_execution.9c05a384226d4180.js deleted file mode 100644 index 3c798de..0000000 --- a/web/halo_execution.9c05a384226d4180.js +++ /dev/null @@ -1,10 +0,0 @@ -import { app } from "../../scripts/app.js"; -import { mountHaloExecutionNode } from "./halo.3bc35993e091e5c5.mjs"; -const ids = new Set(["MATRIXSpectralSampler", "MATRIX_CropTailPaste", "MATRIX_EyeMask", "MATRIX_LatentTail", "MATRIX_OutputStage", "MATRIX_PhotoFinisher", "MATRIX_SaveClean", "MATRIX_SkinMask"]); -const attach = (node) => { - if (ids.has(node.comfyClass || node.type)) mountHaloExecutionNode(node, {app}); -}; -app.registerExtension({ - name: "matrixlab.halo.execution.d5bdc09336581bf5", - nodeCreated: attach, loadedGraphNode: attach, -}); diff --git a/web/halo_resolution.3078c6fb7c5009a2.mjs b/web/halo_resolution.83fe73faf4ac1a41.mjs similarity index 98% rename from web/halo_resolution.3078c6fb7c5009a2.mjs rename to web/halo_resolution.83fe73faf4ac1a41.mjs index 871f38e..6aee4f5 100644 --- a/web/halo_resolution.3078c6fb7c5009a2.mjs +++ b/web/halo_resolution.83fe73faf4ac1a41.mjs @@ -1,4 +1,4 @@ -import { HALO_TOKENS, createHaloWidgetHost, haloMinimumNodeHeight, haloWidgetLayoutHeight, measureHaloContentHeight, measureHaloHorizontalChrome, measureHaloVerticalChrome, mountHaloSurface, setHaloNodeSize } from "./halo.3bc35993e091e5c5.mjs"; +import { HALO_TOKENS, createHaloWidgetHost, haloMinimumNodeHeight, haloWidgetLayoutHeight, measureHaloContentHeight, measureHaloHorizontalChrome, measureHaloVerticalChrome, mountHaloSurface, setHaloNodeSize } from "./halo.d64a9a13ccfeb9ce.mjs"; export { createHaloWidgetHost, haloMinimumNodeHeight, haloWidgetLayoutHeight, measureHaloContentHeight, measureHaloHorizontalChrome, measureHaloVerticalChrome, setHaloNodeSize }; @@ -103,7 +103,7 @@ export function createHaloResolutionDeck(options) { const root = doc.createElement("div"); root.className = "matrixlab-halo-resolution"; root.setAttribute("role", "group"); - root.setAttribute("aria-label", options.ariaLabel || "MATRIXLAB Resolution controls"); + root.setAttribute("aria-label", options.ariaLabel || "MATRIX Resolution controls"); const tier = doc.createElement("div"); tier.className = "matrixlab-halo-resolution__tier"; const tierText = doc.createElement("span"); @@ -193,7 +193,7 @@ export function createHaloResolutionDeck(options) { const width = Number(root.clientWidth) || Math.max(HALO_TOKENS.uiMinWidth, Number(node.size?.[0]) || HALO_TOKENS.uiMinWidth); root.dataset.layout = width >= 560 ? "wide" : "narrow"; tierText.textContent = state.tier || "RESOLUTION"; - profile.textContent = state.profile ?? "MATRIXLAB"; + profile.textContent = state.profile ?? "MATRIX"; profile.style.display = profile.textContent ? "" : "none"; for (const group of groups) { group.grid.style.gridTemplateColumns = `repeat(${Math.min(group.items.length, width >= 560 ? 6 : 3)},minmax(0,1fr))`; @@ -267,9 +267,9 @@ export function calculateTierResolution(aspectRatio, resolutionTier) { export function migrateResolutionGraph(graph) { for (const node of graph?.nodes || []) { const values = node.widgets_values; - const general = node.type === "MATRIXLAB_Resolution" + const general = node.type === "MATRIX_Resolution" && Array.isArray(values) && values.length >= 5 && typeof values[1] === "number"; - const influencer = node.type === "MATRIXLAB_AIInfluencerResolution" + const influencer = node.type === "MATRIX_AIInfluencerResolution" && node.outputs?.some(output => output.name === "render_width"); if (!general && !influencer) continue; const linkedInputs = node.inputs?.some(input => input.link != null); diff --git a/web/image_collection.57c26c1112198635.js b/web/image_collection.4a857a26ffb9735e.js similarity index 97% rename from web/image_collection.57c26c1112198635.js rename to web/image_collection.4a857a26ffb9735e.js index 7ab5846..b065ad3 100644 --- a/web/image_collection.57c26c1112198635.js +++ b/web/image_collection.4a857a26ffb9735e.js @@ -1,10 +1,10 @@ -import { mountImageGallery } from "./gallery.e7ba3b92bd70c1e5.mjs"; -import { createHaloWidgetHost, haloWidgetLayoutHeight, measureHaloContentHeight, setHaloNodeSize } from "./halo.3bc35993e091e5c5.mjs"; +import { mountImageGallery } from "./gallery.1b81fe83387a60e1.mjs"; +import { createHaloWidgetHost, haloWidgetLayoutHeight, measureHaloContentHeight, setHaloNodeSize } from "./halo.d64a9a13ccfeb9ce.mjs"; const { app } = globalThis.comfyAPI?.app || {}; const { api } = globalThis.comfyAPI?.api || {}; -const NODE_TYPE = "MATRIXLAB_ImageBatchLoader"; +const NODE_TYPE = "MATRIX_ImageBatchLoader"; const COLLECTION_WIDGET = "collection"; const PRESENTATION_WIDGET = "matrixlab_image_collection_ui"; const MIN_WIDTH = 420; diff --git a/web/prompt_director.47d51c1218485744.js b/web/prompt_director.402dd6f04f072edf.js similarity index 99% rename from web/prompt_director.47d51c1218485744.js rename to web/prompt_director.402dd6f04f072edf.js index 334a2fb..b544e18 100644 --- a/web/prompt_director.47d51c1218485744.js +++ b/web/prompt_director.402dd6f04f072edf.js @@ -6,13 +6,13 @@ import { measureHaloContentHeight, mountHaloSurface, setHaloNodeSize, -} from "./halo.3bc35993e091e5c5.mjs"; +} from "./halo.d64a9a13ccfeb9ce.mjs"; const { app } = globalThis.comfyAPI?.app || {}; const { api } = globalThis.comfyAPI?.api || {}; -const NODE_TYPE = "MATRIXLAB_PromptDirector"; -const LOADER_TYPE = "MATRIXLAB_ImageBatchLoader"; +const NODE_TYPE = "MATRIX_AutoPrompter"; +const LOADER_TYPE = "MATRIX_ImageBatchLoader"; const PRESENTATION_WIDGET = "matrixlab_prompt_director_ui"; const CREDENTIAL_PATH = "/matrixlab/prompt-director/v1/credential"; const MODELS_PATH = "/matrixlab/prompt-director/v1/models"; @@ -882,7 +882,7 @@ export function mountPromptDirector(node, options = {}) { collection = currentCollection(); if (!collection.items.length) throw new Error("Add 1–10 reference images before generating."); if (REQUIRED_WIDGETS.some((name) => isWidgetLinked(node, name))) { - throw new Error("Disconnect linked Prompt Director fields before generating."); + throw new Error("Disconnect linked Auto Prompter fields before generating."); } requestId = uuid(options.randomUUID); } @@ -1076,7 +1076,7 @@ export function mountPromptDirector(node, options = {}) { presentation = node.addDOMWidget(PRESENTATION_WIDGET, "matrixlab-prompt-director", host, { serialize: false, hideOnZoom: false, getMinHeight: measuredHeight, getHeight: measuredHeight, }); - if (!presentation) throw new Error("ComfyUI did not create the Prompt Director DOM widget"); + if (!presentation) throw new Error("ComfyUI did not create the Auto Prompter DOM widget"); presentation.serialize = false; presentation.options ||= {}; presentation.options.serialize = false; diff --git a/web/resolution_control_deck.83ec2d2da93e1ef9.js b/web/resolution_control_deck.11b94ed50b65d983.js similarity index 97% rename from web/resolution_control_deck.83ec2d2da93e1ef9.js rename to web/resolution_control_deck.11b94ed50b65d983.js index ace213f..ba957be 100644 --- a/web/resolution_control_deck.83ec2d2da93e1ef9.js +++ b/web/resolution_control_deck.11b94ed50b65d983.js @@ -1,7 +1,7 @@ import { app } from "../../scripts/app.js"; -import { createHaloResolutionDeck, createHaloWidgetHost, haloMinimumNodeHeight, haloWidgetLayoutHeight, measureHaloContentHeight, setHaloNodeSize, calculateTierResolution, migrateResolutionGraph } from "./halo_resolution.3078c6fb7c5009a2.mjs"; +import { createHaloResolutionDeck, createHaloWidgetHost, haloMinimumNodeHeight, haloWidgetLayoutHeight, measureHaloContentHeight, setHaloNodeSize, calculateTierResolution, migrateResolutionGraph } from "./halo_resolution.83fe73faf4ac1a41.mjs"; -const NODE_IDS = new Set("MATRIXLAB_Resolution".split(",").filter(Boolean)); +const NODE_IDS = new Set("MATRIX_Resolution".split(",").filter(Boolean)); const RATIOS = { "1:1": [1, 1], "16:9": [16, 9], "9:16": [9, 16], "4:3": [4, 3], "3:4": [3, 4], "3:2": [3, 2], "2:3": [2, 3], "4:5": [4, 5], @@ -112,7 +112,7 @@ function writeState(node, next) { try { widget.callback?.call(widget, value, app.canvas, node, undefined, undefined); } catch (error) { - console.warn("MATRIXLAB Resolution widget callback failed", error); + console.warn("MATRIX Resolution widget callback failed", error); } } node.setDirtyCanvas?.(true, true); @@ -353,7 +353,7 @@ function createDeckElement(node, control) { document, node, control, - ariaLabel: "MATRIXLAB Resolution controls", + ariaLabel: "MATRIX Resolution controls", haloOptions: { app }, groups: [ { id: "ratio", label: "ASPECT", items: ratioItems }, @@ -429,7 +429,7 @@ function install(node) { this.errorMessage = ""; } catch (error) { this.errorMessage = String(error?.message || error); - console.warn("MATRIXLAB Resolution action was refused", error); + console.warn("MATRIX Resolution action was refused", error); this.render?.(); return false; } @@ -445,7 +445,7 @@ function install(node) { try { element = createDeckElement(node, control); host = createHaloWidgetHost(element, document); - if (!host) throw new Error("MATRIXLAB Resolution HALO host unavailable"); + if (!host) throw new Error("MATRIX Resolution HALO host unavailable"); control.host = host; domWidget = node.addDOMWidget("matrixlab_resolution_ui", "matrixlab-prism", host, { serialize: false, @@ -459,7 +459,7 @@ function install(node) { domWidget.serialize = false; domWidget.options ||= {}; domWidget.options.serialize = false; - if (!control.mountHalo?.()) throw new Error("MATRIXLAB Resolution HALO surface unavailable"); + if (!control.mountHalo?.()) throw new Error("MATRIX Resolution HALO surface unavailable"); hideCanonicalWidgets(node, control); node[CONTROL] = control; } catch (error) { @@ -479,7 +479,7 @@ function install(node) { } element?.remove(); host?.remove(); - console.warn("MATRIXLAB Resolution DOM UI unavailable; native widgets remain active", error); + console.warn("MATRIX Resolution DOM UI unavailable; native widgets remain active", error); return null; }