diff --git a/.github/workflows/publish_action.yml b/.github/workflows/publish_action.yml new file mode 100644 index 0000000..ce56ba6 --- /dev/null +++ b/.github/workflows/publish_action.yml @@ -0,0 +1,21 @@ +name: Publish to Comfy registry +on: + workflow_dispatch: + push: + branches: + - main + paths: + - "pyproject.toml" + +jobs: + publish-node: + name: Publish Custom Node to registry + runs-on: ubuntu-latest + if: ${{ github.repository_owner == 'gmorks' }} + steps: + - name: Check out code + uses: actions/checkout@v4 + - name: Publish Custom Node + uses: Comfy-Org/publish-node-action@main + with: + personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }} ## Add your own personal access token to your Github Repository secrets and reference it here. \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1a84de4..c2ea3d1 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,7 @@ config.ini __pycache__/ # Ignore assets folder assets/ -# Ignore node.zip -node.zip \ No newline at end of file +# Ignore references +references/ +# Ignore memory-bank +.memory-bank/ \ No newline at end of file diff --git a/README.md b/README.md index 5cad52b..ac1fc31 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,19 @@ # ComfyUI-SendToDiscord -ComfyUI-SendToDiscord is a custom node for [ComfyUI](https://github.com/comfyanonymous/ComfyUI) that simplifies sending preview images to Discord via webhooks. It supports both single-image uploads and batch mode, making it an efficient tool for sharing your generated images directly with your Discord server. +ComfyUI-SendToDiscord is a custom node for [ComfyUI](https://github.com/comfyanonymous/ComfyUI) that simplifies sending preview images to Discord via webhooks. It supports both single-image uploads and batch mode with intelligent fallback, automatic compression, and workflow preservation, making it an efficient and robust tool for sharing your generated images directly with your Discord server. ![Node Preview](examples/workflow.png "Node Example") - Workflow included in the image ## Features -- Send individual or batches of images directly to a Discord channel via webhooks. -- Includes metadata (e.g., prompts and additional PNG information) in saved images. -- Configurable batch size and compression level. -- Simple setup with a `config.ini` file for webhook URL configuration. +- **Smart Image Delivery**: Send individual or batches of images with intelligent fallback (individual sending if batch fails) +- **Automatic Compression**: WebP compression with configurable quality to optimize file sizes +- **Workflow Preservation**: Automatically preserves ComfyUI workflow JSON when compressing images +- **Metadata Support**: Includes prompts and additional PNG information in saved images +- **Advanced Configuration**: Extensive configuration options for compression, fallback, and file size limits +- **Visual Status Indicators**: Real-time progress indicators with emoji feedback +- **Robust Error Handling**: Graceful handling of network issues and Discord API limits +- **Output Node Compatibility**: Functions as both preview and output node for flexible workflow integration ## Requirements - Python 3.10+ @@ -51,9 +55,9 @@ comfy node registry-install comfyui-sendtodiscord ### Dependencies This node has been tested with the following Python packages: -- `Pillow>=9.5.0` -- `requests>=2.28.2` -- `numpy>=1.23.5` +- `Pillow>=11.0.0` (updated for better WebP support and performance) +- `requests>=2.32.0` (updated for security improvements) +- `numpy>=1.26.0` (updated for compatibility) If you encounter issues, please ensure these versions or higher are installed, or consult the official ComfyUI documentation for compatible dependencies. @@ -64,15 +68,47 @@ If you encounter issues, please ensure these versions or higher are installed, o 3. Configure the node parameters: - **Send to Discord**: Enable or disable image uploads to Discord. - **Batch Mode**: Enable to accumulate images and send them in a single batch. + - **Passthrough Image** (optional): Input for passing images through to other nodes. 4. Generate your images, and they will be uploaded automatically to the specified Discord channel. ## Configuration -The node uses the following parameters: +### Node Parameters - **images**: The list of images to process. - **send_to_discord**: Enable or disable sending images to Discord (enable/disable). - **batch_mode**: Accumulate images and send them as a batch (enable/disable). +- **passthrough_image** (optional): Input for chaining with other nodes. + +### Advanced Configuration (config.ini) + +The node supports extensive configuration options in your `config.ini` file: + +```ini +[Discord] +webhook_url = your-webhook-url-here + +[Fallback] +# Enable automatic fallback (individual sending if batch fails) +enable_fallback = true + +# Enable automatic image compression +enable_compression = true + +# Compression quality (0-100, higher = better quality) +compression_quality = 80 + +# Maximum file size before compression (MB) +max_file_size_mb = 8 +``` + +### Key Features + +- **Intelligent Fallback**: If batch sending fails, automatically attempts individual sending +- **WebP Compression**: Automatically compresses large images while preserving quality +- **Workflow Preservation**: When images are compressed, the original ComfyUI workflow JSON is automatically sent alongside +- **Visual Feedback**: Console messages with emoji indicators show upload progress and status +- **Flexible Integration**: Works as both a preview node and output node for maximum workflow compatibility ## Example Workflow diff --git a/config.ini.example b/config.ini.example new file mode 100644 index 0000000..9eaccc3 --- /dev/null +++ b/config.ini.example @@ -0,0 +1,46 @@ +# Example configuration file for ComfyUI-SendToDiscord +# Copy this file as 'config.ini' and configure your values + +[Discord] +# Discord webhook URL (REQUIRED) +# Get your webhook from: Server Settings > Integrations > Webhooks +webhook_url = https://discord.com/api/webhooks/YOUR_WEBHOOK_ID/YOUR_WEBHOOK_TOKEN + +[Fallback] +# Enable automatic fallback (individual sending if batch fails) +# Values: true/false +enable_fallback = true + +# Enable automatic image compression +# Values: true/false +# NOTE: Disabling this preserves original quality and metadata +# WARNING: When compression is enabled, ComfyUI workflow metadata will be lost +# due to WebP format conversion. Disable compression to preserve workflows. +enable_compression = true + +# Compression quality (only if enable_compression = true) +# Values: 1-100 (100 = maximum quality, larger size) +# Recommended: 80 for quality/size balance +compression_quality = 80 + +# Maximum file size before applying compression (in MB) +# Discord has a 25MB total limit per message +# Recommended: 8MB to leave margin for batch sending +max_file_size_mb = 8 + +# RECOMMENDED CONFIGURATION FOR DIFFERENT CASES: +# +# For maximum quality (lossless): +# enable_compression = false +# enable_fallback = true +# +# For quality/speed balance: +# enable_compression = true +# compression_quality = 80 +# enable_fallback = true +# +# For maximum speed: +# enable_compression = true +# compression_quality = 60 +# enable_fallback = true +# max_file_size_mb = 5 \ No newline at end of file diff --git a/config.ini.template b/config.ini.template deleted file mode 100644 index 730123e..0000000 --- a/config.ini.template +++ /dev/null @@ -1,6 +0,0 @@ -; Configuration file for ComfyUI-SendToDiscord -; Replace 'your-webhook-url-here' with your Discord webhook URL. -; You can get a webhook URL by going to your Discord server settings, then Integrations, then Webhooks, then New Webhook. - -[Discord] -webhook_url = your-webhook-url-here \ No newline at end of file diff --git a/examples/workflow.json b/examples/workflow.json new file mode 100644 index 0000000..562fffa --- /dev/null +++ b/examples/workflow.json @@ -0,0 +1,415 @@ +{ + "id": "d8b98eb0-bccc-4302-97f2-b19f5de444f9", + "revision": 0, + "last_node_id": 10, + "last_link_id": 10, + "nodes": [ + { + "id": 3, + "type": "KSampler", + "pos": [ + 863, + 186 + ], + "size": [ + 315, + 474 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "name": "model", + "type": "MODEL", + "link": 1 + }, + { + "name": "positive", + "type": "CONDITIONING", + "link": 4 + }, + { + "name": "negative", + "type": "CONDITIONING", + "link": 6 + }, + { + "name": "latent_image", + "type": "LATENT", + "link": 2 + } + ], + "outputs": [ + { + "name": "LATENT", + "type": "LATENT", + "slot_index": 0, + "links": [ + 7 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.43", + "Node name for S&R": "KSampler" + }, + "widgets_values": [ + 748480492901144, + "randomize", + 20, + 8, + "euler", + "normal", + 1 + ] + }, + { + "id": 5, + "type": "EmptyLatentImage", + "pos": [ + 473, + 609 + ], + "size": [ + 315, + 106 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "LATENT", + "type": "LATENT", + "slot_index": 0, + "links": [ + 2 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.43", + "Node name for S&R": "EmptyLatentImage" + }, + "widgets_values": [ + 512, + 512, + 3 + ] + }, + { + "id": 6, + "type": "CLIPTextEncode", + "pos": [ + 415, + 186 + ], + "size": [ + 422.84503173828125, + 164.31304931640625 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "name": "clip", + "type": "CLIP", + "link": 3 + } + ], + "outputs": [ + { + "name": "CONDITIONING", + "type": "CONDITIONING", + "slot_index": 0, + "links": [ + 4 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.43", + "Node name for S&R": "CLIPTextEncode" + }, + "widgets_values": [ + "beautiful scenery nature glass bottle landscape, , purple galaxy bottle," + ] + }, + { + "id": 7, + "type": "CLIPTextEncode", + "pos": [ + 413, + 389 + ], + "size": [ + 425.27801513671875, + 180.6060791015625 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "name": "clip", + "type": "CLIP", + "link": 5 + } + ], + "outputs": [ + { + "name": "CONDITIONING", + "type": "CONDITIONING", + "slot_index": 0, + "links": [ + 6 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.43", + "Node name for S&R": "CLIPTextEncode" + }, + "widgets_values": [ + "text, watermark" + ] + }, + { + "id": 8, + "type": "VAEDecode", + "pos": [ + 890, + 730 + ], + "size": [ + 210, + 46 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "name": "samples", + "type": "LATENT", + "link": 7 + }, + { + "name": "vae", + "type": "VAE", + "link": 8 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "slot_index": 0, + "links": [ + 10 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.43", + "Node name for S&R": "VAEDecode" + }, + "widgets_values": [] + }, + { + "id": 10, + "type": "PreviewImageWithDiscord", + "pos": [ + 1220, + 210 + ], + "size": [ + 870, + 480 + ], + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 10 + }, + { + "name": "passthrough_image", + "shape": 7, + "type": "IMAGE", + "link": null + } + ], + "outputs": [ + { + "name": "image", + "type": "IMAGE", + "links": null + } + ], + "properties": { + "cnr_id": "comfyui-sendtodiscord", + "ver": "17abe1485a2a724f44767543a6e6e870eaa32884", + "Node name for S&R": "PreviewImageWithDiscord" + }, + "widgets_values": [ + "enable", + "enable" + ] + }, + { + "id": 4, + "type": "CheckpointLoaderSimple", + "pos": [ + 26, + 474 + ], + "size": [ + 315, + 98 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "slot_index": 0, + "links": [ + 1 + ] + }, + { + "name": "CLIP", + "type": "CLIP", + "slot_index": 1, + "links": [ + 3, + 5 + ] + }, + { + "name": "VAE", + "type": "VAE", + "slot_index": 2, + "links": [ + 8 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.43", + "Node name for S&R": "CheckpointLoaderSimple" + }, + "widgets_values": [ + "SD1.5\\v1-5-pruned-emaonly.ckpt" + ] + } + ], + "links": [ + [ + 1, + 4, + 0, + 3, + 0, + "MODEL" + ], + [ + 2, + 5, + 0, + 3, + 3, + "LATENT" + ], + [ + 3, + 4, + 1, + 6, + 0, + "CLIP" + ], + [ + 4, + 6, + 0, + 3, + 1, + "CONDITIONING" + ], + [ + 5, + 4, + 1, + 7, + 0, + "CLIP" + ], + [ + 6, + 7, + 0, + 3, + 2, + "CONDITIONING" + ], + [ + 7, + 3, + 0, + 8, + 0, + "LATENT" + ], + [ + 8, + 4, + 2, + 8, + 1, + "VAE" + ], + [ + 10, + 8, + 0, + 10, + 0, + "IMAGE" + ] + ], + "groups": [], + "config": {}, + "extra": { + "ds": { + "scale": 0.7513148009015777, + "offset": [ + 74, + -86 + ] + }, + "node_versions": { + "comfy-core": "0.3.12", + "ComfyUI-SendToDiscord": "860cd507a569a680e488d65eb23a9dc0d2d158df" + }, + "ue_links": [], + "frontendVersion": "1.23.4" + }, + "version": 0.4 +} \ No newline at end of file diff --git a/examples/workflow.png b/examples/workflow.png index c464bf7..6b6b65d 100644 Binary files a/examples/workflow.png and b/examples/workflow.png differ diff --git a/log.log b/log.log new file mode 100644 index 0000000..fc7a357 --- /dev/null +++ b/log.log @@ -0,0 +1,120 @@ + +# ComfyUI Error Report +## Error Details +- **Node ID:** N/A +- **Node Type:** N/A +- **Exception Type:** Prompt execution failed +- **Exception Message:** Prompt has no outputs: +## Stack Trace +``` +PromptExecutionError@http://127.0.0.1:8188/assets/index-CWzmkThr.js:120930:5 +queuePrompt@http://127.0.0.1:8188/assets/index-CWzmkThr.js:121273:13 + +``` +## System Information +- **ComfyUI Version:** 0.3.43 +- **Arguments:** main.py +- **OS:** nt +- **Python Version:** 3.12.9 | packaged by Anaconda, Inc. | (main, Feb 6 2025, 18:49:16) [MSC v.1929 64 bit (AMD64)] +- **Embedded Python:** false +- **PyTorch Version:** 2.7.1+cu126 +## Devices + +- **Name:** cuda:0 NVIDIA GeForce RTX 3060 : native + - **Type:** cuda + - **VRAM Total:** 12884246528 + - **VRAM Free:** 11788091392 + - **Torch VRAM Total:** 0 + - **Torch VRAM Free:** 0 + +## Logs +``` +2025-07-07T23:32:20.547599 - [START] Security scan2025-07-07T23:32:20.547599 - +2025-07-07T23:32:21.749098 - [DONE] Security scan2025-07-07T23:32:21.749098 - +2025-07-07T23:32:21.880842 - ## ComfyUI-Manager: installing dependencies done.2025-07-07T23:32:21.880842 - +2025-07-07T23:32:21.881841 - ** ComfyUI startup time:2025-07-07T23:32:21.881841 - 2025-07-07T23:32:21.881841 - 2025-07-07 23:32:21.8812025-07-07T23:32:21.881841 - +2025-07-07T23:32:21.881841 - ** Platform:2025-07-07T23:32:21.881841 - 2025-07-07T23:32:21.881841 - Windows2025-07-07T23:32:21.881841 - +2025-07-07T23:32:21.881841 - ** Python version:2025-07-07T23:32:21.881841 - 2025-07-07T23:32:21.881841 - 3.12.9 | packaged by Anaconda, Inc. | (main, Feb 6 2025, 18:49:16) [MSC v.1929 64 bit (AMD64)]2025-07-07T23:32:21.881841 - +2025-07-07T23:32:21.881841 - ** Python executable:2025-07-07T23:32:21.882841 - 2025-07-07T23:32:21.882841 - F:\Alex\Desktop\comfyui_node_dev\.venv\Scripts\python.exe2025-07-07T23:32:21.882841 - +2025-07-07T23:32:21.882841 - ** ComfyUI Path:2025-07-07T23:32:21.882841 - 2025-07-07T23:32:21.882841 - F:\ComfyUI\2025-07-07T23:32:21.882841 - +2025-07-07T23:32:21.882841 - ** ComfyUI Base Folder Path:2025-07-07T23:32:21.882841 - 2025-07-07T23:32:21.882841 - F:\ComfyUI\2025-07-07T23:32:21.882841 - +2025-07-07T23:32:21.882841 - ** User directory:2025-07-07T23:32:21.883841 - 2025-07-07T23:32:21.883841 - F:\Alex\Desktop\comfyui_node_dev\ComfyUI\user2025-07-07T23:32:21.883841 - +2025-07-07T23:32:21.883841 - ** ComfyUI-Manager config path:2025-07-07T23:32:21.883841 - 2025-07-07T23:32:21.883841 - F:\Alex\Desktop\comfyui_node_dev\ComfyUI\user\default\ComfyUI-Manager\config.ini2025-07-07T23:32:21.883841 - +2025-07-07T23:32:21.883841 - ** Log path:2025-07-07T23:32:21.883841 - 2025-07-07T23:32:21.883841 - F:\Alex\Desktop\comfyui_node_dev\ComfyUI\user\comfyui.log2025-07-07T23:32:21.883841 - +2025-07-07T23:32:23.286623 - +Prestartup times for custom nodes: +2025-07-07T23:32:23.286623 - 3.7 seconds: F:\Alex\Desktop\comfyui_node_dev\ComfyUI\custom_nodes\ComfyUI-Manager +2025-07-07T23:32:23.286623 - +2025-07-07T23:32:25.149195 - Checkpoint files will always be loaded safely. +2025-07-07T23:32:25.250105 - Total VRAM 12287 MB, total RAM 65457 MB +2025-07-07T23:32:25.251106 - pytorch version: 2.7.1+cu126 +2025-07-07T23:32:25.252104 - Set vram state to: NORMAL_VRAM +2025-07-07T23:32:25.252104 - Device: cuda:0 NVIDIA GeForce RTX 3060 : native +2025-07-07T23:32:26.996762 - Using pytorch attention +2025-07-07T23:32:29.400084 - Python version: 3.12.9 | packaged by Anaconda, Inc. | (main, Feb 6 2025, 18:49:16) [MSC v.1929 64 bit (AMD64)] +2025-07-07T23:32:29.400084 - ComfyUI version: 0.3.43 +2025-07-07T23:32:29.446467 - ComfyUI frontend version: 1.23.4 +2025-07-07T23:32:29.448469 - [Prompt Server] web root: F:\Alex\Desktop\comfyui_node_dev\.venv\Lib\site-packages\comfyui_frontend_package\static +2025-07-07T23:32:30.660864 - State loaded successfully +2025-07-07T23:32:30.661861 - CSV history loaded successfully +2025-07-07T23:32:30.716029 - jjk_util loding2025-07-07T23:32:30.716029 - +2025-07-07T23:32:30.726574 - ### Loading: ComfyUI-Manager (V3.33.8) +2025-07-07T23:32:30.726574 - [ComfyUI-Manager] network_mode: public +2025-07-07T23:32:31.055303 - ### ComfyUI Version: v0.3.43-26-ge740dfd8 | Released on '2025-07-07' +2025-07-07T23:32:31.356145 - +Import times for custom nodes: +2025-07-07T23:32:31.356145 - 0.0 seconds: F:\Alex\Desktop\comfyui_node_dev\ComfyUI\custom_nodes\websocket_image_save.py +2025-07-07T23:32:31.356145 - 0.0 seconds: F:\Alex\Desktop\comfyui_node_dev\ComfyUI\custom_nodes\comfyui-display-value +2025-07-07T23:32:31.356145 - 0.0 seconds: F:\Alex\Desktop\comfyui_node_dev\ComfyUI\custom_nodes\ComfyUI-Jjk-Nodes +2025-07-07T23:32:31.356145 - 0.0 seconds: F:\Alex\Desktop\comfyui_node_dev\ComfyUI\custom_nodes\comfyui-custom-scripts +2025-07-07T23:32:31.356145 - 0.0 seconds: F:\Alex\Desktop\comfyui_node_dev\ComfyUI\custom_nodes\ComfyUI-SendToDiscord +2025-07-07T23:32:31.356145 - 0.3 seconds: F:\Alex\Desktop\comfyui_node_dev\ComfyUI\custom_nodes\ComfyUI-Animagine-Prompt +2025-07-07T23:32:31.357142 - 0.6 seconds: F:\Alex\Desktop\comfyui_node_dev\ComfyUI\custom_nodes\ComfyUI-Manager +2025-07-07T23:32:31.357142 - +2025-07-07T23:32:31.476630 - [ComfyUI-Manager] default cache updated: https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/model-list.json +2025-07-07T23:32:31.488645 - [ComfyUI-Manager] default cache updated: https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/alter-list.json +2025-07-07T23:32:31.523703 - [ComfyUI-Manager] default cache updated: https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/github-stats.json +2025-07-07T23:32:31.578315 - [ComfyUI-Manager] default cache updated: https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/custom-node-list.json +2025-07-07T23:32:31.623887 - [ComfyUI-Manager] default cache updated: https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/extension-node-map.json +2025-07-07T23:32:32.088087 - Context impl SQLiteImpl. +2025-07-07T23:32:32.088087 - Will assume non-transactional DDL. +2025-07-07T23:32:32.089602 - No target revision found. +2025-07-07T23:32:32.120644 - Starting server + +2025-07-07T23:32:32.121647 - To see the GUI go to: http://127.0.0.1:8188 +2025-07-07T23:32:34.608182 - FETCH ComfyRegistry Data: 5/912025-07-07T23:32:34.608182 - +2025-07-07T23:32:37.664521 - FETCH ComfyRegistry Data: 10/912025-07-07T23:32:37.665521 - +2025-07-07T23:32:40.772723 - FETCH ComfyRegistry Data: 15/912025-07-07T23:32:40.772723 - +2025-07-07T23:32:43.495940 - got prompt +2025-07-07T23:32:43.495940 - invalid prompt: {'type': 'prompt_no_outputs', 'message': 'Prompt has no outputs', 'details': '', 'extra_info': {}} +2025-07-07T23:32:43.806274 - FETCH ComfyRegistry Data: 20/912025-07-07T23:32:43.807269 - +2025-07-07T23:32:46.859044 - FETCH ComfyRegistry Data: 25/912025-07-07T23:32:46.859044 - +2025-07-07T23:32:49.846180 - FETCH ComfyRegistry Data: 30/912025-07-07T23:32:49.846180 - +2025-07-07T23:32:53.627295 - FETCH ComfyRegistry Data: 35/912025-07-07T23:32:53.627295 - +2025-07-07T23:32:56.712527 - FETCH ComfyRegistry Data: 40/912025-07-07T23:32:56.712527 - +2025-07-07T23:33:00.369889 - FETCH ComfyRegistry Data: 45/912025-07-07T23:33:00.370888 - +2025-07-07T23:33:03.418851 - FETCH ComfyRegistry Data: 50/912025-07-07T23:33:03.418851 - +2025-07-07T23:33:06.536026 - FETCH ComfyRegistry Data: 55/912025-07-07T23:33:06.537028 - +2025-07-07T23:33:09.630203 - FETCH ComfyRegistry Data: 60/912025-07-07T23:33:09.630203 - +2025-07-07T23:33:13.402533 - FETCH ComfyRegistry Data: 65/912025-07-07T23:33:13.403534 - +2025-07-07T23:33:17.131215 - FETCH ComfyRegistry Data: 70/912025-07-07T23:33:17.131215 - +2025-07-07T23:33:20.199542 - FETCH ComfyRegistry Data: 75/912025-07-07T23:33:20.199542 - +2025-07-07T23:33:23.359758 - FETCH ComfyRegistry Data: 80/912025-07-07T23:33:23.359758 - +2025-07-07T23:33:26.526590 - FETCH ComfyRegistry Data: 85/912025-07-07T23:33:26.526590 - +2025-07-07T23:33:29.536282 - FETCH ComfyRegistry Data: 90/912025-07-07T23:33:29.536282 - +2025-07-07T23:33:30.642835 - FETCH ComfyRegistry Data [DONE]2025-07-07T23:33:30.642835 - +2025-07-07T23:33:30.788265 - [ComfyUI-Manager] default cache updated: https://api.comfy.org/nodes +2025-07-07T23:33:30.810303 - FETCH DATA from: https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/custom-node-list.json2025-07-07T23:33:30.810303 - 2025-07-07T23:33:30.987707 - [DONE]2025-07-07T23:33:30.987707 - +2025-07-07T23:33:31.053282 - [ComfyUI-Manager] All startup tasks have been completed. +2025-07-07T23:33:53.215932 - got prompt +2025-07-07T23:33:53.215932 - invalid prompt: {'type': 'prompt_no_outputs', 'message': 'Prompt has no outputs', 'details': '', 'extra_info': {}} + +``` +## Attached Workflow +Please make sure that workflow does not contain any sensitive information such as API keys or passwords. +``` +{"id":"00000000-0000-0000-0000-000000000000","revision":0,"last_node_id":6,"last_link_id":7,"nodes":[{"id":6,"type":"ImageBatch","pos":[482.96026611328125,357.0851135253906],"size":[140,46],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"image1","name":"image1","type":"IMAGE","link":5},{"localized_name":"image2","name":"image2","type":"IMAGE","link":6}],"outputs":[{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","links":[7]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.43","Node name for S&R":"ImageBatch"},"widgets_values":[]},{"id":4,"type":"PreviewImageWithDiscord","pos":[680.9085083007812,354.81817626953125],"size":[263.9666748046875,344],"flags":{},"order":3,"mode":0,"inputs":[{"localized_name":"images","name":"images","type":"IMAGE","link":7},{"localized_name":"send_to_discord","name":"send_to_discord","type":"COMBO","widget":{"name":"send_to_discord"},"link":null},{"localized_name":"batch_mode","name":"batch_mode","type":"COMBO","widget":{"name":"batch_mode"},"link":null}],"outputs":[{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","links":[]}],"properties":{"cnr_id":"comfyui-sendtodiscord","ver":"17abe1485a2a724f44767543a6e6e870eaa32884","Node name for S&R":"PreviewImageWithDiscord"},"widgets_values":["enable","disable"]},{"id":5,"type":"LoadImage","pos":[116.22103118896484,718.6907958984375],"size":[274,314],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"image","name":"image","type":"COMBO","widget":{"name":"image"},"link":null},{"localized_name":"choose file to upload","name":"upload","type":"IMAGEUPLOAD","widget":{"name":"upload"},"link":null}],"outputs":[{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","links":[5]},{"localized_name":"MASK","name":"MASK","type":"MASK","links":null}],"properties":{"cnr_id":"comfy-core","ver":"0.3.43","Node name for S&R":"LoadImage"},"widgets_values":["ComfyUI_temp_ilxtz_00011_.png","image"]},{"id":2,"type":"LoadImage","pos":[108,332],"size":[274,314],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"image","name":"image","type":"COMBO","widget":{"name":"image"},"link":null},{"localized_name":"choose file to upload","name":"upload","type":"IMAGEUPLOAD","widget":{"name":"upload"},"link":null}],"outputs":[{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","links":[6]},{"localized_name":"MASK","name":"MASK","type":"MASK","links":null}],"properties":{"cnr_id":"comfy-core","ver":"0.3.43","Node name for S&R":"LoadImage"},"widgets_values":["ComfyUI_00045_.png","image"]}],"links":[[5,5,0,6,0,"IMAGE"],[6,2,0,6,1,"IMAGE"],[7,6,0,4,0,"IMAGE"]],"groups":[],"config":{},"extra":{"ds":{"scale":1.1,"offset":[87.01042563636398,-245.8818633636361]}},"version":0.4} +``` + +## Additional Context +(Please add any additional context or steps to reproduce the error here) diff --git a/media/icon.svg b/media/icon.svg index 8f4d8cf..9f78278 100644 --- a/media/icon.svg +++ b/media/icon.svg @@ -1,24 +1,15 @@ - - - - - - - - - - - - - + + + + + + + + + + + + \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 0c3ab42..846b9e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,13 +1,13 @@ [project] name = "comfyui-sendtodiscord" description = "ComfyUI-SendToDiscord is a custom node for ComfyUI that simplifies sending preview images to Discord via webhooks. It supports both single-image uploads and batch mode, making it an efficient tool for sharing your generated images directly with your Discord server." -version = "1.0.0" +version = "1.2.0" license = {file = "LICENSE"} dependencies = ["Pillow>=9.5.0", "requests>=2.28.2", "numpy>=1.23.5"] [project.urls] Repository = "https://github.com/gmorks/ComfyUI-SendToDiscord" -# Used by Comfy Registry https://comfyregistry.org +# Used by Comfy Registry https://registry.comfy.org [tool.comfy] PublisherId = "gmorks" diff --git a/requirements.txt b/requirements.txt index 745b083..2743304 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -Pillow>=9.5.0 -requests>=2.28.2 -numpy>=1.23.5 \ No newline at end of file +Pillow>=11.0.0 +requests>=2.32.0 +numpy>=1.26.0 \ No newline at end of file diff --git a/send_to_discord.py b/send_to_discord.py index 1bbee03..a405b4b 100644 --- a/send_to_discord.py +++ b/send_to_discord.py @@ -29,29 +29,119 @@ class PreviewImageWithDiscord: - Sets the output directory for temporary images using `folder_paths.get_temp_directory()`. - Defines a prefix to be appended to image filenames for uniqueness. - - Loads the webhook URL from a configuration file. + - Loads the webhook URL and configuration from a configuration file. """ self.output_dir = folder_paths.get_temp_directory() # Ensure this is thread-safe if used in a multithreaded environment self.type = "temp" self.prefix_append = "_temp_" + ''.join(random.choice("abcdefghijklmnopqrstupvxyz") for x in range(5)) self.compress_level = 1 - self.webhook_url = self.load_webhook_url() + self.config = self.load_config() + self.webhook_url = self.config.get('Discord', 'webhook_url', fallback='') self.batch_size = 5 # Number of images to accumulate before sending self.image_queue = [] + self.last_status = "Ready" # Last send status + + # Fallback and compression configurations + self.enable_fallback = self.config.getboolean('Fallback', 'enable_fallback', fallback=True) + self.enable_compression = self.config.getboolean('Fallback', 'enable_compression', fallback=True) + self.compression_quality = self.config.getint('Fallback', 'compression_quality', fallback=80) + self.max_file_size_mb = self.config.getfloat('Fallback', 'max_file_size_mb', fallback=8.0) - def load_webhook_url(self): + def load_config(self): """ - Loads the webhook URL from a configuration file. + Loads the complete configuration from a configuration file. Returns: - str: The webhook URL if found, otherwise an empty string. + ConfigParser: The configuration object with all settings loaded. """ config = configparser.ConfigParser() config_path = os.path.join(os.path.dirname(__file__), 'config.ini') config.read(config_path) - # Retrieve the webhook URL from the Discord section in the configuration file - webhook_url = config.get('Discord', 'webhook_url', fallback='') - return webhook_url + return config + + def get_file_size_mb(self, file_path): + """ + Gets the size of a file in megabytes. + + Args: + file_path (str): Path to the file. + + Returns: + float: File size in MB. + """ + return os.path.getsize(file_path) / (1024 * 1024) + + def compress_image(self, image_path): + """ + Compresses an image to WebP format to reduce file size. + + Args: + image_path (str): Path to the original image file. + + Returns: + str: Path to the compressed file. + + Raises: + Exception: If there's an error during compression. + """ + print("⚠️ Warning: Compressing to WebP will remove workflow metadata from PNG") + + try: + with Image.open(image_path) as img: + # Convert to appropriate mode for WebP + if img.mode in ('RGBA', 'LA', 'P'): + if img.mode == 'P' and 'transparency' in img.info: + img = img.convert('RGBA') + elif img.mode == 'LA': + img = img.convert('RGBA') + else: + img = img.convert('RGB') + + # Generate compressed filename + base_name = os.path.splitext(os.path.basename(image_path))[0] + compressed_path = os.path.join(os.path.dirname(image_path), f"{base_name}_compressed.webp") + + # Save with WebP compression + img.save(compressed_path, 'WEBP', quality=self.compression_quality, optimize=True) + + return compressed_path + + except Exception as e: + raise Exception(f"Error compressing image: {e}") + + def _get_mime_type(self, file_path): + """ + Determines MIME type based on file extension. + + Args: + file_path (str): Path to the file. + + Returns: + str: MIME type string. + """ + ext = file_path.lower() + if ext.endswith('.webp'): + return 'image/webp' + elif ext.endswith(('.jpg', '.jpeg')): + return 'image/jpeg' + else: + return 'image/png' + + def _cleanup_temp_files(self, current_path, original_path, workflow_path): + """ + Cleans up temporary files created during processing. + + Args: + current_path (str): Path to the current (possibly compressed) image. + original_path (str): Path to the original image. + workflow_path (str): Path to the workflow JSON file. + """ + for path in [current_path, workflow_path]: + if path and path != original_path and os.path.exists(path): + try: + os.remove(path) + except OSError: + pass @classmethod def INPUT_TYPES(s): @@ -65,15 +155,18 @@ class PreviewImageWithDiscord: {"images": ("IMAGE", ), "send_to_discord": (["enable", "disable"], {"default": "disable"}), "batch_mode": (["enable", "disable"], {"default": "disable"})}, + "optional": + {"passthrough_image": ("IMAGE", )}, "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"}, } - RETURN_TYPES = () + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) FUNCTION = "preview_images" OUTPUT_NODE = True CATEGORY = "image" - def preview_images(self, images, send_to_discord="disable", batch_mode="disable", prompt=None, extra_pnginfo=None): + def preview_images(self, images, send_to_discord="disable", batch_mode="disable", passthrough_image=None, prompt=None, extra_pnginfo=None): """ Previews images and optionally sends them to Discord. @@ -81,11 +174,12 @@ class PreviewImageWithDiscord: images (list): List of images to preview. send_to_discord (str): Whether to send images to Discord ("enable" or "disable"). batch_mode (str): Whether to send images in batch mode ("enable" or "disable"). + passthrough_image (list, optional): Optional image input for passthrough. prompt (str, optional): Prompt text to add as metadata. extra_pnginfo (dict, optional): Additional PNG info to add as metadata. Returns: - dict: A dictionary containing the UI results with image details. + dict: A dictionary containing the UI results with image details and optional image output. """ full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path("ComfyUI", self.output_dir, images[0].shape[1], images[0].shape[0]) results = list() @@ -112,12 +206,18 @@ class PreviewImageWithDiscord: # Send image to Discord if enabled if send_to_discord == "enable" and self.webhook_url: + # Extract workflow JSON if available + workflow_json = None + if extra_pnginfo and 'workflow' in extra_pnginfo: + workflow_json = extra_pnginfo['workflow'] + if batch_mode == "enable": - self.image_queue.append(full_path) + # Store both image path and workflow JSON for batch processing + self.image_queue.append((full_path, file, workflow_json)) if len(self.image_queue) >= self.batch_size: self.send_batch_to_discord() else: - self.send_to_discord(full_path, file) + self.send_to_discord(full_path, file, workflow_json) counter += 1 @@ -125,50 +225,224 @@ class PreviewImageWithDiscord: if send_to_discord == "enable" and batch_mode == "enable" and self.image_queue: self.send_batch_to_discord() - return { "ui": { "images": results } } + # Log workflow JSON if available (useful for debugging and potential future features) + if extra_pnginfo and 'workflow' in extra_pnginfo: + print(f"📋 Workflow JSON detected: {len(str(extra_pnginfo['workflow']))} characters") + # Optionally log the workflow to a file for analysis + try: + workflow_log_path = os.path.join(self.output_dir, "workflow_log.json") + with open(workflow_log_path, 'w') as f: + json.dump(extra_pnginfo['workflow'], f, indent=2) + print(f"📁 Workflow saved to: {workflow_log_path}") + except Exception as e: + print(f"⚠️ Could not save workflow: {e}") - def send_to_discord(self, image_path, filename): + # Return appropriate output based on whether passthrough is used + output_image = passthrough_image if passthrough_image is not None else images + return {"ui": {"images": results}, "result": (output_image,)} + + def send_to_discord(self, image_path, filename, workflow_json=None): """ - Sends a single image to Discord. + Sends a single image to Discord with intelligent fallback and compression. Args: image_path (str): The path to the image file. filename (str): The name of the image file. + workflow_json (dict, optional): Workflow JSON to send alongside compressed images. """ - with open(image_path, 'rb') as img_file: - files = { - 'file': (filename, img_file, 'image/png') - } - + self.last_status = "📤 Sending..." + current_path = image_path + attempts = [] + workflow_path = None + + # Check if compression is needed + if self.enable_compression and self.get_file_size_mb(current_path) > self.max_file_size_mb: try: - response = requests.post(self.webhook_url, files=files) - if response.status_code == 200: - print(f"Successfully sent image to Discord: {filename}") - else: - print(f"Failed to send image to Discord: {filename}. Status: {response.status_code}") + current_path = self.compress_image(current_path) + attempts.append(f"Compressed to {self.get_file_size_mb(current_path):.1f}MB") + + # Create workflow JSON file if available + if workflow_json: + base_name = os.path.splitext(filename)[0] + workflow_filename = f"{base_name}_workflow.json" + workflow_path = os.path.join(os.path.dirname(current_path), workflow_filename) + + try: + with open(workflow_path, 'w') as f: + json.dump(workflow_json, f, indent=2) + attempts.append("+ workflow JSON") + print("📋 Workflow JSON will be sent alongside compressed image") + except Exception as e: + print(f"⚠️ Could not create workflow file: {e}") + workflow_path = None + except Exception as e: - print(f"Error sending image to Discord: {e}") + print(f"Error compressing image: {e}") + + # Attempt sending + success = self._attempt_send_single(current_path, filename, workflow_path) + + if success: + self.last_status = "✅ Sent" + if attempts: + print(f"Image sent successfully: {filename} ({', '.join(attempts)})") + else: + print(f"Image sent successfully: {filename}") + else: + self.last_status = "❌ Error" + print(f"Error sending image: {filename}") + + # Clean up temporary files + self._cleanup_temp_files(current_path, image_path, workflow_path) + + def _attempt_send_single(self, image_path, filename, workflow_path=None): + """ + Attempts to send a single image to Discord, optionally with workflow JSON file. + + Args: + image_path (str): Path to the image file. + filename (str): Name of the file. + workflow_path (str, optional): Path to the workflow JSON file. + + Returns: + bool: True if sending was successful, False otherwise. + """ + try: + # Determine MIME type + mime_type = self._get_mime_type(image_path) + + files = {} + + # Add image file + with open(image_path, 'rb') as img_file: + files['file'] = (filename, img_file.read(), mime_type) + + # Add workflow JSON file if available + if workflow_path and os.path.exists(workflow_path): + with open(workflow_path, 'rb') as workflow_file: + files['file1'] = (os.path.basename(workflow_path), workflow_file.read(), 'application/json') + + response = requests.post(self.webhook_url, files=files, timeout=30) + return response.status_code == 200 + + except Exception as e: + print(f"Error in individual send: {e}") + return False def send_batch_to_discord(self): """ - Sends a batch of images to Discord. + Sends a batch of images to Discord with intelligent fallback. """ - files = {} - for i, image_path in enumerate(self.image_queue): - with open(image_path, 'rb') as img_file: - filename = os.path.basename(image_path) - files[f'file{i}'] = (filename, img_file.read(), 'image/png') + if not self.image_queue: + return + + self.last_status = f"📤 Sending batch ({len(self.image_queue)} images)..." - try: - response = requests.post(self.webhook_url, files=files) - if response.status_code == 200: - print(f"Successfully sent batch of {len(self.image_queue)} images to Discord") + # Try batch sending first + batch_success = self._attempt_send_batch() + + if batch_success: + self.last_status = f"✅ Batch sent ({len(self.image_queue)} images)" + print(f"Batch of {len(self.image_queue)} images sent successfully") + elif self.enable_fallback: + # Fallback: send one by one + self.last_status = "🔄 Fallback: sending individually..." + print("Fallback activated: sending images individually") + + success_count = 0 + for image_data in self.image_queue: + image_path, filename, workflow_json = image_data + if self._attempt_send_single_for_batch(image_path, filename, workflow_json): + success_count += 1 + + if success_count == len(self.image_queue): + self.last_status = f"✅ Sent individually ({success_count}/{len(self.image_queue)})" else: - print(f"Failed to send batch to Discord. Status: {response.status_code}") - except Exception as e: - print(f"Error sending batch to Discord: {e}") + self.last_status = f"⚠️ Partial ({success_count}/{len(self.image_queue)})" + + print(f"Fallback completed: {success_count}/{len(self.image_queue)} images sent") + else: + self.last_status = "❌ Batch error" + print("Error sending batch and fallback disabled") self.image_queue.clear() + + def _attempt_send_batch(self): + """ + Attempts to send a batch of images to Discord. + + Returns: + bool: True if sending was successful, False otherwise. + """ + try: + files = {} + total_size = 0 + + for i, image_data in enumerate(self.image_queue): + image_path, filename, workflow_json = image_data + file_size = self.get_file_size_mb(image_path) + total_size += file_size + + # Check Discord limit (25MB total) + if total_size > 25: + print(f"Batch exceeds 25MB ({total_size:.1f}MB), activating fallback") + return False + + with open(image_path, 'rb') as img_file: + # Determine MIME type + mime_type = self._get_mime_type(image_path) + files[f'file{i}'] = (filename, img_file.read(), mime_type) + + response = requests.post(self.webhook_url, files=files, timeout=60) + return response.status_code == 200 + + except Exception as e: + print(f"Error in batch send: {e}") + return False + + def _attempt_send_single_for_batch(self, image_path, filename, workflow_json=None): + """ + Attempts to send an individual image as part of batch fallback. + + Args: + image_path (str): Path to the image file. + filename (str): Name of the file. + workflow_json (dict, optional): Workflow JSON to send alongside compressed images. + + Returns: + bool: True if sending was successful, False otherwise. + """ + current_path = image_path + workflow_path = None + + # Apply compression if enabled and necessary + if self.enable_compression and self.get_file_size_mb(current_path) > self.max_file_size_mb: + try: + current_path = self.compress_image(current_path) + + # If we compressed and have workflow JSON, create a temporary JSON file + if workflow_json: + base_name = os.path.splitext(filename)[0] + workflow_filename = f"{base_name}_workflow.json" + workflow_path = os.path.join(os.path.dirname(current_path), workflow_filename) + + try: + with open(workflow_path, 'w') as f: + json.dump(workflow_json, f, indent=2) + except Exception as e: + print(f"⚠️ Could not create workflow file for batch fallback: {e}") + workflow_path = None + + except Exception as e: + print(f"Error compressing {filename}: {e}") + + # Attempt sending + success = self._attempt_send_single(current_path, filename, workflow_path) + + # Clean up temporary files + self._cleanup_temp_files(current_path, image_path, workflow_path) + + return success # Node class mappings NODE_CLASS_MAPPINGS = {