├── .gitignore ├── README.md ├── __init__.py ├── assets ├── play_sound_node.jpg ├── system_notif.jpg ├── system_notif_node.jpg ├── unified_notif_node.jpg └── webhook_node.jpg ├── nodes ├── play_sound.py ├── system_notification.py ├── unified_notification.py ├── util.py └── webhook.py ├── pyproject.toml └── web ├── assets └── notify.mp3 ├── play_sound.js ├── system_notification.js ├── unified_notification.js └── util.js /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ComfyUI Notifications 2 | 3 | This package provides custom nodes to ComfyUI to notify users when workflows have completed. 4 | 5 | These tools are also available via [ComfyUI-Custom-Scripts](https://github.com/pythongosssss/ComfyUI-Custom-Scripts), but this package comes without the bloat of other tools. 6 | 7 | ## Send Notification 8 | 9 | Sends a system notification via the browser. 10 | 11 | 12 | 13 | 14 | 15 | 16 | ## Play Sound 17 | 18 | Plays a chime sound to notify the user. 19 | 20 | 21 | 22 | 23 | ## Webhook 24 | 25 | Send a webhook to the specified URL. Supports customizing JSON body with the `json_format` template. 26 | 27 | 28 | 29 | ## Unified Notification 30 | 31 | Supports multiple notification types in one node for convenience. 32 | 33 | 34 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | from .nodes.play_sound import PlaySound 2 | from .nodes.system_notification import SystemNotification 3 | from .nodes.unified_notification import UnifiedNotification 4 | from .nodes.webhook import Webhook 5 | 6 | NODE_CLASS_MAPPINGS = { 7 | "Notif-PlaySound": PlaySound, 8 | "Notif-SystemNotification": SystemNotification, 9 | "Notif-UnifiedNotification": UnifiedNotification, 10 | "Notif-Webhook": Webhook, 11 | } 12 | 13 | NODE_DISPLAY_NAME_MAPPINGS = { 14 | "Notif-PlaySound": "Play Sound", 15 | "Notif-SystemNotification": "System Notification", 16 | "Notif-UnifiedNotification": "Unified Notification", 17 | "Notif-Webhook": "Webhook", 18 | } 19 | 20 | WEB_DIRECTORY = "./web" 21 | -------------------------------------------------------------------------------- /assets/play_sound_node.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/royceschultz/ComfyUI-Notifications/c587ede2dca28b83428b932340df54115dd30441/assets/play_sound_node.jpg -------------------------------------------------------------------------------- /assets/system_notif.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/royceschultz/ComfyUI-Notifications/c587ede2dca28b83428b932340df54115dd30441/assets/system_notif.jpg -------------------------------------------------------------------------------- /assets/system_notif_node.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/royceschultz/ComfyUI-Notifications/c587ede2dca28b83428b932340df54115dd30441/assets/system_notif_node.jpg -------------------------------------------------------------------------------- /assets/unified_notif_node.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/royceschultz/ComfyUI-Notifications/c587ede2dca28b83428b932340df54115dd30441/assets/unified_notif_node.jpg -------------------------------------------------------------------------------- /assets/webhook_node.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/royceschultz/ComfyUI-Notifications/c587ede2dca28b83428b932340df54115dd30441/assets/webhook_node.jpg -------------------------------------------------------------------------------- /nodes/play_sound.py: -------------------------------------------------------------------------------- 1 | from .util import ComfyAnyType 2 | 3 | class PlaySound: 4 | @classmethod 5 | def INPUT_TYPES(cls): 6 | return { 7 | "required": { 8 | "any": (ComfyAnyType('*'), {}), 9 | "mode": (["always", "on empty queue"], {}), 10 | "volume": ("FLOAT", {"min": 0, "max": 1, "step": 0.1, "default": 0.5}), 11 | "file": ("STRING", { "default": "notify.mp3" }) 12 | }, 13 | } 14 | 15 | FUNCTION = "nop" 16 | OUTPUT_NODE = True 17 | RETURN_TYPES = tuple() 18 | CATEGORY = "notifications" 19 | 20 | def nop(self, any, mode, volume, file): 21 | return {"ui": {"a": []}, "result": (0, )} 22 | -------------------------------------------------------------------------------- /nodes/system_notification.py: -------------------------------------------------------------------------------- 1 | from .util import ComfyAnyType 2 | 3 | class SystemNotification: 4 | @classmethod 5 | def INPUT_TYPES(cls): 6 | return { 7 | "required": { 8 | "any": (ComfyAnyType("*"), {}), 9 | "mode": (["always", "on empty queue"], {}), 10 | "notification_text": ('STRING', {'default': 'Your notification has triggered.'}), 11 | }, 12 | } 13 | 14 | FUNCTION = "nop" 15 | OUTPUT_NODE = True 16 | RETURN_TYPES = tuple() 17 | CATEGORY = "notifications" 18 | 19 | def nop(self, any, mode, notification_text): 20 | return {"ui": {"notification_text": [notification_text]}, "result": (0, )} 21 | -------------------------------------------------------------------------------- /nodes/unified_notification.py: -------------------------------------------------------------------------------- 1 | from .util import ComfyAnyType 2 | import json 3 | 4 | class UnifiedNotification: 5 | @classmethod 6 | def INPUT_TYPES(cls): 7 | return { 8 | "required": { 9 | "any": (ComfyAnyType("*"), {}), 10 | "mode": (["always", "on empty queue"], {}), 11 | "system_notification": ("BOOLEAN", {"default": True}), 12 | "notification_text": ('STRING', {'default': 'Your notification has triggered.'}), 13 | "play_sound": ("BOOLEAN", {"default": True}), 14 | "volume": ("FLOAT", {"min": 0, "max": 1, "step": 0.1, "default": 0.5}), 15 | "file": ("STRING", { "default": "notify.mp3" }), 16 | }, 17 | } 18 | 19 | FUNCTION = "nop" 20 | OUTPUT_NODE = True 21 | RETURN_TYPES = tuple() 22 | CATEGORY = "notifications" 23 | 24 | def nop(self, *args, **kwargs): 25 | for k, v in kwargs.items(): 26 | kwargs[k] = [v] # UI expects an iterable for some reason 27 | del kwargs['any'] # Avoid passing large objects 28 | kwargs['args'] = args 29 | return {"ui": kwargs, "result": (0, )} 30 | -------------------------------------------------------------------------------- /nodes/util.py: -------------------------------------------------------------------------------- 1 | # Hack: string type that is always equal in not equal comparisons 2 | # Thanks to https://github.com/pythongosssss/ComfyUI-Custom-Scripts 3 | class ComfyAnyType(str): 4 | def __ne__(self, __value: object) -> bool: 5 | return False 6 | -------------------------------------------------------------------------------- /nodes/webhook.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | from server import PromptServer 4 | 5 | from .util import ComfyAnyType 6 | 7 | class Webhook: 8 | @classmethod 9 | def INPUT_TYPES(cls): 10 | return { 11 | "required": { 12 | "any": (ComfyAnyType("*"), {}), 13 | "mode": (["always", "on empty queue"], {}), 14 | "webhook_url": ("STRING", {'default': 'http://localhost:5000/'}), 15 | "verify_ssl": ("BOOLEAN", {"default": True}), 16 | "notification_text": ('STRING', {'default': 'Your notification has triggered.'}), 17 | "json_format": ('STRING', {'default': '{"text": ""}'}), 18 | "timeout": ('FLOAT', {'default': 3, 'min': 0, 'max': 60}), 19 | }, 20 | } 21 | 22 | FUNCTION = 'hook' 23 | OUTPUT_NODE = True 24 | RETURN_TYPES = tuple() 25 | CATEGORY = "notifications" 26 | 27 | def hook(self, any, mode, webhook_url, notification_text, json_format, timeout, verify_ssl): 28 | if mode == 'on empty queue': 29 | queue = PromptServer.instance.prompt_queue.queue 30 | queue_len = len(queue) 31 | if queue_len > 0: 32 | print(f'[Webhook Notificaton] Not sending notification, queue is not empty (size: {queue_len})') 33 | return (0, ) 34 | payload = json_format.replace("", notification_text) 35 | payload = json.loads(payload) 36 | res = requests.post(webhook_url, json=payload, timeout=timeout, verify=verify_ssl) 37 | res.raise_for_status() 38 | return (0, ) 39 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "comfyui-notifications" 3 | description = "Send notifications when a workflow completes." 4 | version = "1.0.0" 5 | license = "MIT License" 6 | 7 | [project.urls] 8 | Repository = "https://github.com/royceschultz/ComfyUI-Notifications" 9 | # Used by Comfy Registry https://comfyregistry.org 10 | 11 | [tool.comfy] 12 | PublisherId = "royceschultz" 13 | DisplayName = "ComfyUI-Notifications" 14 | Icon = "" 15 | -------------------------------------------------------------------------------- /web/assets/notify.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/royceschultz/ComfyUI-Notifications/c587ede2dca28b83428b932340df54115dd30441/web/assets/notify.mp3 -------------------------------------------------------------------------------- /web/play_sound.js: -------------------------------------------------------------------------------- 1 | import { app } from '../../../scripts/app.js' 2 | import { playSound, appQueueIsEmpty } from './util.js' 3 | 4 | app.registerExtension({ 5 | name: 'Notifications.PlaySound', 6 | async beforeRegisterNodeDef(nodeType, nodeData, app) { 7 | if (nodeData.name === 'Notif-PlaySound') { 8 | const onExecuted = nodeType.prototype.onExecuted 9 | nodeType.prototype.onExecuted = async function () { 10 | onExecuted?.apply(this, arguments) 11 | if (this.widgets[0].value === 'on empty queue') { 12 | if (!await appQueueIsEmpty(app)) return 13 | } 14 | let file = this.widgets[2].value 15 | playSound(file, this.widgets[1].value) 16 | } 17 | } 18 | }, 19 | }) 20 | -------------------------------------------------------------------------------- /web/system_notification.js: -------------------------------------------------------------------------------- 1 | import { app } from '../../../scripts/app.js' 2 | import { 3 | notificationSetup, sendNotification, getNotificationTextFromArguments, 4 | appQueueIsEmpty, 5 | } from './util.js' 6 | 7 | app.registerExtension({ 8 | name: 'Notifications.SystemNotification', 9 | async beforeRegisterNodeDef(nodeType, nodeData, app) { 10 | if (nodeData.name === 'Notif-SystemNotification') { 11 | const onExecuted = nodeType.prototype.onExecuted 12 | nodeType.prototype.onExecuted = async function () { 13 | onExecuted?.apply(this, arguments) 14 | if (this.widgets[0].value === 'on empty queue') { 15 | if (!await appQueueIsEmpty(app)) return 16 | } 17 | if (!notificationSetup()) return 18 | const notification_text = getNotificationTextFromArguments(arguments) 19 | sendNotification('ComfyUI', notification_text) 20 | } 21 | 22 | const onNodeCreated = nodeType.prototype.onNodeCreated 23 | nodeType.prototype.onNodeCreated = function () { 24 | onNodeCreated?.apply(this, arguments) 25 | notificationSetup() 26 | } 27 | } 28 | }, 29 | }) 30 | -------------------------------------------------------------------------------- /web/unified_notification.js: -------------------------------------------------------------------------------- 1 | import { app } from '../../../scripts/app.js' 2 | import { 3 | notificationSetup, sendNotification, getNotificationTextFromArguments, 4 | playSound, 5 | appQueueIsEmpty, 6 | } from './util.js' 7 | 8 | 9 | app.registerExtension({ 10 | name: 'Notifications.UnifiedNotification', 11 | async beforeRegisterNodeDef(nodeType, nodeData, app) { 12 | if (nodeData.name === 'Notif-UnifiedNotification') { 13 | const onExecuted = nodeType.prototype.onExecuted 14 | nodeType.prototype.onExecuted = async function () { 15 | onExecuted?.apply(this, arguments) 16 | const args = arguments[0] ?? {} 17 | if (args.mode[0] === 'on empty queue') { 18 | if(!await appQueueIsEmpty(app)) return 19 | } 20 | if (args.system_notification[0]) { 21 | const notif_text = getNotificationTextFromArguments(arguments) 22 | sendNotification('ComfyUI', notif_text) 23 | } 24 | if (args.play_sound[0]) { 25 | playSound(args.file[0], args.volume[0] ?? 0.5) 26 | } 27 | } 28 | 29 | const onNodeCreated = nodeType.prototype.onNodeCreated 30 | nodeType.prototype.onNodeCreated = function () { 31 | onNodeCreated?.apply(this, arguments) 32 | notificationSetup() 33 | } 34 | } 35 | }, 36 | }) 37 | -------------------------------------------------------------------------------- /web/util.js: -------------------------------------------------------------------------------- 1 | export const notificationSetup = () => { 2 | if (!('Notification' in window)) { 3 | console.log('This browser does not support notifications.') 4 | alert('This browser does not support notifications.') 5 | return 6 | } 7 | if (Notification.permission === 'denied') { 8 | console.log('Notifications are blocked. Please enable them in your browser settings.') 9 | alert('Notifications are blocked. Please enable them in your browser settings.') 10 | return 11 | } 12 | if (Notification.permission !== 'granted') { 13 | Notification.requestPermission() 14 | } 15 | return true 16 | } 17 | 18 | export const sendNotification = (title, body) => { 19 | if (!notificationSetup()) return 20 | const notification = new Notification(title, { body }) 21 | return notification 22 | } 23 | 24 | export const getNotificationTextFromArguments = (args) => { 25 | return args[0]?.notification_text[0] ?? 'Your notification has triggered.' 26 | } 27 | 28 | export const playSound = (file, volume) => { 29 | if (!file) { 30 | file = 'notify.mp3' 31 | } 32 | if (!file.startsWith('http')) { 33 | if (!file.includes('/')) { 34 | file = 'assets/' + file 35 | } 36 | file = new URL(file, import.meta.url) 37 | } 38 | const url = new URL(file) 39 | const audio = new Audio(url) 40 | audio.volume = volume 41 | audio.play() 42 | } 43 | 44 | export const appQueueIsEmpty = async (app) => { 45 | if (app.ui.lastQueueSize !== 0) { 46 | await new Promise((r) => setTimeout(r, 500)) 47 | } 48 | if (app.ui.lastQueueSize !== 0) { 49 | return false 50 | } 51 | return true 52 | } 53 | --------------------------------------------------------------------------------