├── image ├── node1.jpg ├── node2.jpg └── setting1.jpg ├── video ├── is18age.MP3 ├── しゅ~りょ~。.wav ├── ちぃーッス!.wav ├── ringtone1.mp3 ├── ringtone2.mp3 ├── ringtone3.mp3 ├── ringtone4.mp3 ├── ringtone5.mp3 ├── ringtone6.mp3 └── Let's speak Chinese.MP3 ├── .gitignore ├── pyproject.toml ├── .github └── workflows │ └── publish.yml ├── readme.zh.md ├── web ├── addfranc.js ├── utils.js └── menu.js ├── readme.md └── __init__.py /image/node1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgldlk/ComfyUI-PC-ding-dong/HEAD/image/node1.jpg -------------------------------------------------------------------------------- /image/node2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgldlk/ComfyUI-PC-ding-dong/HEAD/image/node2.jpg -------------------------------------------------------------------------------- /video/is18age.MP3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgldlk/ComfyUI-PC-ding-dong/HEAD/video/is18age.MP3 -------------------------------------------------------------------------------- /video/しゅ~りょ~。.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgldlk/ComfyUI-PC-ding-dong/HEAD/video/しゅ~りょ~。.wav -------------------------------------------------------------------------------- /video/ちぃーッス!.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgldlk/ComfyUI-PC-ding-dong/HEAD/video/ちぃーッス!.wav -------------------------------------------------------------------------------- /image/setting1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgldlk/ComfyUI-PC-ding-dong/HEAD/image/setting1.jpg -------------------------------------------------------------------------------- /video/ringtone1.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgldlk/ComfyUI-PC-ding-dong/HEAD/video/ringtone1.mp3 -------------------------------------------------------------------------------- /video/ringtone2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgldlk/ComfyUI-PC-ding-dong/HEAD/video/ringtone2.mp3 -------------------------------------------------------------------------------- /video/ringtone3.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgldlk/ComfyUI-PC-ding-dong/HEAD/video/ringtone3.mp3 -------------------------------------------------------------------------------- /video/ringtone4.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgldlk/ComfyUI-PC-ding-dong/HEAD/video/ringtone4.mp3 -------------------------------------------------------------------------------- /video/ringtone5.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgldlk/ComfyUI-PC-ding-dong/HEAD/video/ringtone5.mp3 -------------------------------------------------------------------------------- /video/ringtone6.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgldlk/ComfyUI-PC-ding-dong/HEAD/video/ringtone6.mp3 -------------------------------------------------------------------------------- /video/Let's speak Chinese.MP3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lgldlk/ComfyUI-PC-ding-dong/HEAD/video/Let's speak Chinese.MP3 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.py[cod] 3 | /output/ 4 | /input/ 5 | !/input/example.png 6 | /models/ 7 | /temp/ 8 | !custom_nodes/example_node.py.example 9 | extra_model_paths.yaml 10 | /.vs 11 | .vscode/ 12 | .idea/ 13 | venv/ 14 | .venv/ 15 | /web/extensions/* 16 | !/web/extensions/logging.js.example 17 | !/web/extensions/core/ 18 | /tests-ui/data/object_info.json 19 | /user/ 20 | *.log 21 | web_custom_versions/ 22 | .DS_Store 23 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "comfyui-pc-ding-dong" 3 | description = "Just like when your pizza is ready and the oven goes 'Ding! 🍕', this plugin lets your ComfyUI notify you when your AI creations are done baking!\nA ComfyUI custom node that sends you a friendly 'ding-dong' notification when your workflows are fully cooked and ready to serve. No more staring at the screen waiting - let the AI kitchen tell you when dinner's ready! 👨‍🍳" 4 | version = "1.0.0" 5 | license = {file = "LICENSE"} 6 | 7 | [project.urls] 8 | Repository = "https://github.com/lgldlk/ComfyUI-PC-ding-dong" 9 | # Used by Comfy Registry https://comfyregistry.org 10 | 11 | [tool.comfy] 12 | PublisherId = "lgldl" 13 | DisplayName = "ComfyUI-PC-ding-dong" 14 | Icon = "https://tikolu.net/emojimix/emojis/231a.svg" 15 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to Comfy registry 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - main 7 | - master 8 | paths: 9 | - "pyproject.toml" 10 | 11 | jobs: 12 | publish-node: 13 | name: Publish Custom Node to registry 14 | runs-on: ubuntu-latest 15 | # if this is a forked repository. Skipping the workflow. 16 | if: github.event.repository.fork == false 17 | steps: 18 | - name: Check out code 19 | uses: actions/checkout@v4 20 | - name: Publish Custom Node 21 | uses: Comfy-Org/publish-node-action@main 22 | with: 23 | ## Add your own personal access token to your Github Repository secrets and reference it here. 24 | personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }} 25 | -------------------------------------------------------------------------------- /readme.zh.md: -------------------------------------------------------------------------------- 1 | # ⏰ ComfyUI 工作流通知插件 2 | 3 | 就像当披萨烤好时烤箱会发出"叮!🍕"的提示音一样,这个插件可以在你的 AI 创作完成时通知你! 4 | 5 | 这是一个 ComfyUI 自定义节点,当你的工作流程完成时会发送友好的"叮咚"通知。不用再盯着屏幕等待 - 让 AI 厨房告诉你作品什么时候准备好!👨‍🍳 6 | 7 | https://private-user-images.githubusercontent.com/57523724/381594271-c6c4978d-cfbb-4e03-a656-cb91a346ab50.mp4 8 | 9 | 10 | ## 功能特点 🌟 11 | 12 | - 任务完成时获得通知 13 | - 节省云端和本地计算资源 14 | - 完美适用于长时间运行的工作流和批量处理 15 | - 释放你的时间 - 无需盯着屏幕 16 | - 通过及时提醒提高工作流效率 17 | 18 | 19 | 20 | ![设置](./image/setting1.jpg) 21 | 22 | - 工作流完成时的声音通知 23 | - 可调节的通知音量 24 | - 声音开关控制 25 | - 支持单个工作流或批量工作流完成通知 26 | - 支持自定义通知音效上传 27 | 28 | ## 节点 29 | 30 | ### DingDong 节点 31 | 32 | ![叮咚](./image/node1.jpg) 33 | 34 | 一个通知节点,当工作流执行到该节点时会播放自定义音效。你可以选择自己的音频文件并调节音量,获得你想要的通知方式。 35 | 36 | ### DingDongText 节点 37 | 38 | ![叮咚文本](./image/node2.jpg) 39 | 40 | 一个文字转语音的通知节点,当工作流执行到该节点时会朗读自定义文本。语音可以通过调节音调、速度和音量来自定义。 41 | 42 | ## 安装方法 43 | 44 | 1. 下载节点文件 45 | 2. 放置在 ComfyUI 的 `custom_nodes` 文件夹中 46 | 3. 重启 ComfyUI 47 | -------------------------------------------------------------------------------- /web/addfranc.js: -------------------------------------------------------------------------------- 1 | const langScript = document.createElement('script'); 2 | langScript.type = 'module'; 3 | langScript.innerHTML = ` 4 | import { franc, } from 'https://cdn.jsdelivr.net/npm/franc-min@6.2.0/+esm'; 5 | window.pcFranc=franc 6 | `; 7 | document.head.appendChild(langScript); 8 | 9 | export const francMap = { 10 | cmn: 'zh-CN', 11 | spa: 'es', 12 | eng: 'en-GB', 13 | rus: 'ru', 14 | arb: 'ar', 15 | ben: 'bn', 16 | hin: 'hi', 17 | por: 'pt', 18 | ind: 'id', 19 | jpn: 'ja', 20 | fra: 'fr', 21 | deu: 'de', 22 | jav: 'jv', 23 | kor: 'ko', 24 | tel: 'te', 25 | vie: 'vi', 26 | mar: 'mr', 27 | ita: 'it', 28 | tam: 'ta', 29 | tur: 'tr', 30 | urd: 'ur', 31 | guj: 'gu', 32 | pol: 'pl', 33 | ukr: 'uk', 34 | kan: 'kn', 35 | mai: 'mai', 36 | mal: 'ml', 37 | pes: 'fa', 38 | mya: 'my', 39 | swh: 'sw', 40 | sun: 'su', 41 | ron: 'ro', 42 | pan: 'pa', 43 | bho: 'bho', 44 | amh: 'am', 45 | hau: 'ha', 46 | fuv: 'fuv', 47 | bos: 'bs', 48 | hrv: 'hr', 49 | nld: 'nl', 50 | srp: 'sr', 51 | tha: 'th', 52 | ckb: 'ku', 53 | yor: 'yo', 54 | uzn: 'uz', 55 | zlm: 'ms', 56 | ibo: 'ig', 57 | npi: 'ne', 58 | ceb: 'ceb', 59 | skr: 'skr', 60 | tgl: 'tl', 61 | hun: 'hu', 62 | azj: 'az', 63 | sin: 'si', 64 | koi: 'koi', 65 | ell: 'el', 66 | ces: 'cs', 67 | mag: 'mag', 68 | run: 'rn', 69 | bel: 'be', 70 | plt: 'mg', 71 | qug: 'qug', 72 | mad: 'mad', 73 | nya: 'ny', 74 | zyb: 'za', 75 | pbu: 'ps', 76 | kin: 'rw', 77 | zul: 'zu', 78 | bul: 'bg', 79 | swe: 'sv', 80 | lin: 'ln', 81 | som: 'so', 82 | hms: 'hms', 83 | hnj: 'hnj', 84 | ilo: 'ilo', 85 | jpn: 'ja', 86 | kaz: 'kk', 87 | }; 88 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # ⏰ ComfyUI Workflow Notification Plugin 2 | [中文版](./readme.zh.md) 3 | 4 | 5 | Just like when your pizza is ready and the oven goes "Ding! 🍕", this plugin lets your ComfyUI notify you when your AI creations are done baking! 6 | 7 | A ComfyUI custom node that sends you a friendly "ding-dong" notification when your workflows are fully cooked and ready to serve. No more staring at the screen waiting - let the AI kitchen tell you when dinner's ready! 👨‍🍳 8 | 9 | https://private-user-images.githubusercontent.com/57523724/381594271-c6c4978d-cfbb-4e03-a656-cb91a346ab50.mp4 10 | 11 | ## Features 🌟 12 | 13 | - Get notified when tasks complete 14 | - Save cloud and local computing resources 15 | - Perfect for long-running workflows and batch processing 16 | - Free up your time - no need to watch the screen 17 | - Improve workflow efficiency with timely alerts 18 | 19 | 20 | 21 | 22 | ![setting](./image/setting1.jpg) 23 | 24 | - Sound notifications when workflows complete 25 | - Adjustable notification volume 26 | - Sound toggle control 27 | - Support for single workflow or batch workflow completion notifications 28 | - Support for custom notification sound upload 29 | - Save cloud and local computing resources 30 | - Perfect for long-running workflows and batch processing 31 | - Free up your time - no need to watch the screen 32 | - Improve workflow efficiency with timely alerts 33 | 34 | ## Nodes 35 | 36 | ### DingDong Node 37 | 38 | ![dingdong](./image/node1.jpg) 39 | 40 | A notification node that plays a custom sound file when workflow execution reaches it. You can select your own audio file and adjust the volume to get notified exactly how you want. 41 | 42 | ### DingDongText Node 43 | 44 | ![dingdong](./image/node2.jpg) 45 | 46 | A text-to-speech notification node that speaks custom text when workflow execution reaches it. The speech can be customized with adjustable pitch, speed and volume settings. 47 | 48 | 49 | ## Installation 50 | 51 | 1. Download the node files 52 | 2. Place in ComfyUI's `custom_nodes` folder 53 | 3. Restart ComfyUI 54 | -------------------------------------------------------------------------------- /web/utils.js: -------------------------------------------------------------------------------- 1 | import { api } from '../../scripts/api.js'; 2 | import { francMap } from './addfranc.js'; 3 | export async function request(url, method, data) { 4 | let formData = undefined; 5 | if (method === 'POST') { 6 | formData = new FormData(); 7 | if (data) { 8 | for (const [key, value] of Object.entries(data)) { 9 | formData.append(key, value); 10 | } 11 | } 12 | } else { 13 | url += '?' + new URLSearchParams(data).toString(); 14 | } 15 | 16 | return api.fetchApi(url, { method, body: formData }); 17 | } 18 | 19 | const AudioContext = window.AudioContext || window.webkitAudioContext; 20 | const audioContext = new AudioContext(); 21 | let currentSource = null; 22 | 23 | export async function fetchAndPlayAudioSingle(filename, volume = 1) { 24 | // Stop any currently playing audio 25 | if (currentSource) { 26 | try { 27 | currentSource.stop(); 28 | currentSource.disconnect(); 29 | } catch (e) {} 30 | currentSource = null; 31 | } 32 | 33 | const response = await request(`/pc_get_audio`, 'GET', { filename }); 34 | const audioBuffer = await audioContext.decodeAudioData(await response.arrayBuffer()); 35 | currentSource = audioContext.createBufferSource(); 36 | const gainNode = audioContext.createGain(); 37 | gainNode.gain.value = volume; 38 | currentSource.buffer = audioBuffer; 39 | currentSource.connect(gainNode); 40 | gainNode.connect(audioContext.destination); 41 | currentSource.start(); 42 | currentSource.onended = () => { 43 | currentSource = null; 44 | }; 45 | } 46 | 47 | export async function fetchAndPlayAudio(filename, volume = 1) { 48 | const response = await request(`/pc_get_audio`, 'GET', { filename }); 49 | const audioBuffer = await audioContext.decodeAudioData(await response.arrayBuffer()); 50 | 51 | let source = audioContext.createBufferSource(); 52 | const gainNode = audioContext.createGain(); 53 | gainNode.gain.value = volume; 54 | 55 | source.buffer = audioBuffer; 56 | source.connect(gainNode); 57 | gainNode.connect(audioContext.destination); 58 | source.start(); 59 | source.onended = () => { 60 | source = null; 61 | }; 62 | } 63 | 64 | export function get_video_files() { 65 | return request('/pc_get_video_files', 'POST') 66 | .then(async (res) => (await res.json()).video_files) 67 | .catch((err) => { 68 | console.error('Error fetching video files:', err); 69 | return []; 70 | }); 71 | } 72 | 73 | export function set_play_type(play_type) { 74 | return request('/pc_set_play_type', 'POST', { play_type }); 75 | } 76 | 77 | export function play_ding_dong_text(text, pitch, rate, volume) { 78 | const utterance = new SpeechSynthesisUtterance(text); 79 | utterance.pitch = pitch; 80 | utterance.rate = rate; 81 | utterance.volume = volume; 82 | 83 | if (window.pcFranc) { 84 | console.log('🍞 ~ play_ding_dong_text ~ francMap[window.pcFranc(text)]:', window.pcFranc(text)); 85 | utterance.lang = francMap[window.pcFranc(text, { minLength: 3 })]; 86 | } 87 | console.log('🍞 ~ play_ding_dong_text ~ utterance:', utterance); 88 | window.speechSynthesis.cancel(); 89 | window.speechSynthesis.speak(utterance); 90 | } 91 | -------------------------------------------------------------------------------- /web/menu.js: -------------------------------------------------------------------------------- 1 | import { app } from '../../../scripts/app.js'; 2 | 3 | import { api } from '../../scripts/api.js'; 4 | import { $el } from '../../scripts/ui.js'; 5 | import { fetchAndPlayAudioSingle, get_video_files, set_play_type, fetchAndPlayAudio, play_ding_dong_text, request } from './utils.js'; 6 | 7 | const id_prefix = '⏰Ding_Dong'; 8 | const id_music_prefix = `${id_prefix}.music`; 9 | let selectedAudio = null; 10 | let volume = 100; 11 | let open = true; 12 | // all | one 13 | let play_type = 'all'; 14 | let fail_tip = false; 15 | const select_menu_name = `${id_music_prefix}.name`; 16 | const select_menu_name_options = $el('select', { 17 | onchange: (e) => { 18 | const value = e.target.value; 19 | if (value === selectedAudio) { 20 | return; 21 | } 22 | set_select_menu_name_value(value); 23 | }, 24 | }); 25 | 26 | function set_select_menu_name_options(options) { 27 | select_menu_name_options.innerHTML = ''; 28 | options.forEach((item) => { 29 | select_menu_name_options.appendChild($el('option', { value: item.value, textContent: item.text, selected: item.selected })); 30 | }); 31 | } 32 | function set_select_menu_name_value(value) { 33 | selectedAudio = value; 34 | select_menu_name_options.value = value; 35 | app.ui.settings.setSettingValue(select_menu_name, value); 36 | } 37 | 38 | function get_video_files_list() { 39 | return get_video_files().then((res) => 40 | res 41 | .map((item) => ({ 42 | value: item, 43 | text: item, 44 | selected: selectedAudio === item, 45 | })) 46 | .concat({ 47 | value: null, 48 | text: 'None', 49 | selected: selectedAudio === null, 50 | }) 51 | ); 52 | } 53 | 54 | const fileInput = $el('input', { 55 | type: 'file', 56 | accept: '.mp4,.avi,.mov,.mkv,.webm,.mp3', 57 | style: { display: 'none' }, 58 | parent: document.body, 59 | onchange: async () => { 60 | const file = fileInput.files[0]; 61 | const menu_name = `${id_music_prefix}.name`; 62 | const validExtensions = ['.mp4', '.avi', '.mov', '.mkv', '.webm', '.mp3', '.wav']; 63 | if (validExtensions.some((ext) => file.name.toLowerCase().endsWith(ext))) { 64 | const reader = new FileReader(); 65 | reader.onload = async () => {}; 66 | reader.readAsArrayBuffer(file); 67 | const formData = new FormData(); 68 | formData.append('file', file); 69 | 70 | try { 71 | const response = await api.fetchApi('/pc_upload_video', { 72 | method: 'POST', 73 | body: formData, 74 | }); 75 | 76 | const result = await response.json(); 77 | if (result.success) { 78 | set_select_menu_name_options(await get_video_files_list()); 79 | } else { 80 | console.error('Upload failed:', result.error); 81 | } 82 | } catch (err) { 83 | console.error('Error uploading file:', err); 84 | } 85 | } 86 | }, 87 | }); 88 | app.registerExtension({ 89 | name: id_prefix + '.menu', 90 | init() { 91 | get_video_files_list().then(async (res) => { 92 | try { 93 | set_select_menu_name_options(res); 94 | app.ui.settings.addSetting({ 95 | id: `${id_music_prefix}.name`, 96 | name: 'play music name', 97 | tooltip: 'select music name', 98 | defaultValue: res[0].value, 99 | type: () => select_menu_name_options, 100 | onChange(value) { 101 | if (value === selectedAudio) { 102 | return; 103 | } 104 | set_select_menu_name_value(value); 105 | }, 106 | }); 107 | app.ui.settings.addSetting({ 108 | id: `${id_music_prefix}.play`, 109 | name: 'play music select', 110 | type: () => { 111 | return $el('button', { 112 | textContent: 'play', 113 | onclick: () => { 114 | fetchAndPlayAudioSingle(selectedAudio, volume / 100); 115 | }, 116 | }); 117 | }, 118 | }); 119 | } catch (e) { 120 | console.error('get_video_files error', e); 121 | } 122 | }); 123 | api.addEventListener('pc.play_ding_dong_audio', ({ detail }) => { 124 | if (detail.status === 'error' && !fail_tip) { 125 | return; 126 | } 127 | if (selectedAudio && open) { 128 | fetchAndPlayAudioSingle(selectedAudio, volume / 100); 129 | } 130 | }); 131 | api.addEventListener('pc.play_ding_dong_mui', ({ detail }) => { 132 | fetchAndPlayAudio(detail.music, detail.volume / 100); 133 | }); 134 | api.addEventListener('pc.play_ding_dong_text', ({ detail }) => { 135 | play_ding_dong_text(detail.text, detail.pitch, detail.rate, detail.volume); 136 | }); 137 | 138 | app.ui.settings.addSetting({ 139 | id: `${id_music_prefix}.volume`, 140 | name: 'Volume', 141 | type: 'slider', 142 | attrs: { 143 | min: 0, 144 | max: 100, 145 | step: 1, 146 | }, 147 | tooltip: 'set ding dong volume', 148 | defaultValue: 100, 149 | onChange(v) { 150 | volume = v; 151 | }, 152 | }); 153 | 154 | app.ui.settings.addSetting({ 155 | id: `${id_music_prefix}.open`, 156 | name: 'open', 157 | type: 'boolean', 158 | defaultValue: true, 159 | onChange(v) { 160 | open = v; 161 | }, 162 | }); 163 | 164 | app.ui.settings.addSetting({ 165 | id: `${id_music_prefix}.play_type`, 166 | name: 'play type', 167 | tooltip: 'play after all workflows finish or after each single workflow finishes', 168 | type: 'combo', 169 | defaultValue: 'all', 170 | options: [ 171 | { value: 'all', text: 'all' }, 172 | { value: 'one', text: 'one' }, 173 | ], 174 | onChange(v) { 175 | set_play_type(v); 176 | play_type = v; 177 | }, 178 | }); 179 | app.ui.settings.addSetting({ 180 | id: `${id_music_prefix}.upload`, 181 | name: 'upload file', 182 | type: () => { 183 | return $el('button', { 184 | textContent: 'upload', 185 | onclick: () => { 186 | fileInput.click(); 187 | }, 188 | }); 189 | }, 190 | }); 191 | 192 | app.ui.settings.addSetting({ 193 | id: `${id_music_prefix}.fail_tip`, 194 | name: 'fail tip', 195 | tooltip: 'workflow fail tip', 196 | type: 'boolean', 197 | defaultValue: false, 198 | onChange(v) { 199 | fail_tip = v; 200 | }, 201 | }); 202 | }, 203 | }); 204 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | import execution 4 | import asyncio 5 | from typing import List, Literal, NamedTuple, Optional 6 | from server import PromptServer 7 | from aiohttp import web 8 | 9 | routes = PromptServer.instance.routes 10 | 11 | 12 | old_task_done = execution.PromptQueue.task_done 13 | 14 | 15 | play_type = "all" 16 | video_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "video") 17 | 18 | 19 | def new_task_done( 20 | self, item_id, history_result, status: Optional["PromptQueue.ExecutionStatus"] 21 | ): 22 | 23 | ret = old_task_done(self, item_id, history_result, status) 24 | if play_type == "all" and len(self.queue) > 0: 25 | return ret 26 | 27 | PromptServer.instance.send_sync("pc.play_ding_dong_audio", { 28 | "status": status.status_str 29 | }) 30 | 31 | return ret 32 | 33 | 34 | execution.PromptQueue.task_done = new_task_done 35 | 36 | 37 | files_end_with = (".mp4", ".avi", ".mov", ".mkv", ".webm", ".mp3", ".wav") 38 | 39 | 40 | def load_video(): 41 | if not os.path.exists(video_dir): 42 | return [] 43 | 44 | video_files = [] 45 | for file in os.listdir(video_dir): 46 | if file.lower().endswith(files_end_with): 47 | video_files.append(file) 48 | 49 | return video_files 50 | 51 | 52 | local_video_files = load_video() 53 | 54 | 55 | 56 | 57 | @routes.post("/pc_get_video_files") 58 | async def get_video_files(request): 59 | global local_video_files 60 | local_video_files = load_video() 61 | return web.json_response({"video_files": local_video_files}) 62 | 63 | 64 | @routes.get("/pc_get_audio") 65 | async def get_audio(request): 66 | filename = request.query.get("filename") 67 | if not filename: 68 | return web.Response(text="No filename provided", status=400) 69 | 70 | audio_path = os.path.join(video_dir, filename) 71 | 72 | if not os.path.exists(audio_path): 73 | return web.Response(text="Audio file not found", status=404) 74 | 75 | if not filename.lower().endswith(files_end_with): 76 | return web.Response(text="Invalid audio file format", status=400) 77 | 78 | return web.FileResponse(audio_path) 79 | 80 | 81 | @routes.post("/pc_set_play_type") 82 | async def set_play_type(request): 83 | global play_type 84 | the_data = await request.post() 85 | play_type = the_data.get("play_type", "all") 86 | return web.json_response({}) 87 | 88 | 89 | # Handle file upload via aiohttp 90 | @routes.post("/pc_upload_video") 91 | async def upload_video(request): 92 | reader = await request.multipart() 93 | field = await reader.next() 94 | 95 | if not field or field.name != "file": 96 | return web.json_response({"error": "No file uploaded"}, status=400) 97 | 98 | filename = field.filename 99 | if not filename.lower().endswith(files_end_with): 100 | return web.json_response({"error": "Invalid file format"}, status=400) 101 | 102 | # Save uploaded file 103 | if not os.path.exists(video_dir): 104 | os.makedirs(video_dir) 105 | 106 | file_path = os.path.join(video_dir, filename) 107 | 108 | try: 109 | with open(file_path, "wb") as f: 110 | while True: 111 | chunk = await field.read_chunk() 112 | if not chunk: 113 | break 114 | f.write(chunk) 115 | except Exception as e: 116 | return web.json_response({"error": str(e)}, status=500) 117 | 118 | return web.json_response({"success": True, "filename": filename}) 119 | 120 | 121 | class EatAny(str): 122 | def __init__(self): 123 | pass 124 | 125 | def __eq__(self, _): 126 | return True 127 | 128 | def __ne__(self, _): 129 | return False 130 | 131 | 132 | any_type = EatAny() 133 | 134 | 135 | class DingDong: 136 | def __init__(self): 137 | pass 138 | 139 | @classmethod 140 | def INPUT_TYPES(s): 141 | return { 142 | "required": { 143 | "music": ( 144 | local_video_files, 145 | { 146 | "default": ( 147 | local_video_files[0] if len(local_video_files) > 0 else None 148 | ) 149 | }, 150 | ), 151 | "volume": ("FLOAT", {"default": 100, "min": 0, "max": 100, "step": 1}), 152 | "anything": (any_type, {}), 153 | }, 154 | } 155 | 156 | OUTPUT_NODE = True 157 | RETURN_TYPES = (any_type,) 158 | RETURN_NAMES = ("output",) 159 | FUNCTION = "ding_dong" 160 | CATEGORY = "😱 PointAgiClub" 161 | 162 | def ding_dong(self, volume, music, anything): 163 | PromptServer.instance.send_sync( 164 | "pc.play_ding_dong_mui", {"volume": volume, "music": music} 165 | ) 166 | return (anything,) 167 | 168 | 169 | class TimeSleep: 170 | def __init__(self): 171 | pass 172 | 173 | @classmethod 174 | def INPUT_TYPES(s): 175 | return { 176 | "required": { 177 | "seconds": ("FLOAT", {"default": 1, "min": 0, "max": 10, "step": 0.1}), 178 | }, 179 | "optional": { 180 | "anything": (any_type,), 181 | }, 182 | } 183 | 184 | OUTPUT_NODE = True 185 | RETURN_TYPES = (any_type,) 186 | RETURN_NAMES = ("output",) 187 | FUNCTION = "sleep" 188 | CATEGORY = "😱 PointAgiClub" 189 | 190 | def sleep(self, seconds, **anything): 191 | time.sleep(seconds) 192 | return (anything,) 193 | 194 | 195 | class DingDongText: 196 | def __init__(self): 197 | pass 198 | 199 | @classmethod 200 | def INPUT_TYPES(s): 201 | return { 202 | "required": { 203 | "text": ("STRING", {"default": "Hello, World!"}), 204 | "pitch": ( 205 | "FLOAT", 206 | {"default": 1.0, "min": 0.1, "max": 2.0, "step": 0.1}, 207 | ), 208 | "rate": ( 209 | "FLOAT", 210 | {"default": 1.0, "min": 0.1, "max": 10.0, "step": 0.1}, 211 | ), 212 | "volume": ( 213 | "FLOAT", 214 | {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.1}, 215 | ), 216 | "anything": (any_type, {}), 217 | }, 218 | } 219 | 220 | OUTPUT_NODE = True 221 | RETURN_TYPES = (any_type,) 222 | RETURN_NAMES = ("output",) 223 | FUNCTION = "ding_dong" 224 | CATEGORY = "😱 PointAgiClub" 225 | 226 | def ding_dong(self, text, pitch, rate, volume, anything): 227 | 228 | PromptServer.instance.send_sync( 229 | "pc.play_ding_dong_text", 230 | {"text": text, "pitch": pitch, "rate": rate, "volume": volume}, 231 | ) 232 | 233 | return (anything,) 234 | 235 | 236 | NODE_CLASS_MAPPINGS = { 237 | "pc ding dong": DingDong, 238 | "pc ding dong text": DingDongText, 239 | "pc time sleep": TimeSleep, 240 | } 241 | NODE_DISPLAY_NAME_MAPPINGS = { 242 | "pc ding dong": "⏰Ding Dong", 243 | "pc ding dong text": "⏰Ding Dong Text", 244 | "pc time sleep": "⏰Time Sleep", 245 | } 246 | WEB_DIRECTORY = "./web" 247 | --------------------------------------------------------------------------------