├── .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 |
--------------------------------------------------------------------------------