diff --git a/README.md b/README.md index 58366eb..022608f 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ A powerful workflow scheduling extension for ComfyUI that enables automated dail ## 🚀 Installation 安裝方式 ### Method 1: ComfyUI Manager (Recommended) -### 方法 1:ComfyUI 管理器(推薦/稍後可用) +### 方法 1:ComfyUI 管理器(推薦) 1. Open ComfyUI Manager 開啟 ComfyUI 管理器 2. Search for "ComfyUI-ScheduledTask" 搜尋 "ComfyUI-ScheduledTask" 3. Click Install 點擊安裝 @@ -114,6 +114,65 @@ A special utility node that generates time-based random seeds: - Ensures different results for each scheduled run 確保每次排程運行都有不同結果 - Setting this for create large image list and run 在設定時間內進行大規模隨機排程 +### Daily Prompt Scheduler Node / 每日提示詞排程節點 + +![Demo](readme/demo3.jpg) + +A ComfyUI node that provides time-based prompt management with daily rotation capabilities. +提供基於時間的提示詞管理和每日輪換功能。 + +####Features / 功能特色 + +- **Daily Rotation / 每日輪換**: Automatically selects different prompts each day / 每天自動選擇不同的提示詞 +- **Two Operation Modes / 兩種運作模式**: + - **Scheduled Mode / 排程模式**: Sequential selection with time seed (predictable daily progression) / 使用時間種子的順序選擇(可預測的每日進展) + - **Random Mode / 隨機模式**: Random selection with daily seed (random but consistent per day) / 使用每日種子的隨機選擇(隨機但每天一致) +- **File-based Management / 檔案管理**: Load prompts from text files in the `Prompt` folder / 從Prompt資料夾中的文字檔載入提示詞 +- **Individual Time Seeds / 獨立時間種子**: Each text file maintains its own scheduling timeline / 每個文字檔維護自己的排程時間線 +- **List Output / 列表輸出**: Returns prompts as a connectable string list for workflow integration / 返回可連接的字串列表用於工作流程整合 +- **Loop Integration / 迴圈整合**: Provides count output for easy loop control / 提供計數輸出便於迴圈控制 + +#### Setup / 設置 + +1. Visit`Prompt` folder in your node directory: `/ComfyUI-ScheduledTask/Prompt/` / 在節點目錄中訪問`Prompt`資料夾:`/ComfyUI-ScheduledTask/Prompt/` +2. Place your text files (`.txt`) in this folder / 將文字檔(`.txt`)放置在此資料夾中 +3. Each line in the text file represents one prompt (empty lines are ignored) / 文字檔中每一行代表一個提示詞(空行會被忽略) + +#### Inputs / 輸入參數 + +- **txt_file / 文字檔**: Dropdown selection of available text files in the Prompt folder / 下拉選單選擇Prompt資料夾中的可用文字檔 +- **daily_count / 每日數量**: Number of prompts to select each day (1-5000) / 每天選擇的提示詞數量(1-5000) +- **scheduled / 排程**: Toggle between Scheduled and Random modes (Default is Random Mode)/ 在排程模式和隨機模式之間切換,預設為隨機模式,每天隨機抽選提示組數 + +#### Outputs / 輸出參數 + +- **daily_prompts / 每日提示詞**: List of selected prompts (STRING list, connectable to other nodes) / 選中的提示詞列表(字串列表,可連接到其他節點) +- **total_index / 總數索引**: Number of prompts actually returned (INT, useful for loop control) / 實際返回的提示詞數量(整數,用於迴圈控制) + +#### Operation Modes / 運作模式 + +#### Scheduled Mode / 排程模式 +- **First Run / 首次運行**: Creates a time seed based on current date / 基於當前日期創建時間種子 +- **Daily Progression / 每日進展**: Selects prompts sequentially / 按順序選擇提示詞 +- **Example / 範例**: With 12 prompts, selecting 3 per day / 有12個提示詞,每天選擇3個: + - Day 1 / 第1天: Prompts 1, 2, 3 / 提示詞1, 2, 3 + - Day 2 / 第2天: Prompts 4, 5, 6 / 提示詞4, 5, 6 + - Day 3 / 第3天: Prompts 7, 8, 9 / 提示詞7, 8, 9 + - Day 4 / 第4天: Prompts 10, 11, 12 / 提示詞10, 11, 12 + - Day 5 / 第5天: Prompts 1, 2, 3 (cycles back) / 提示詞1, 2, 3(循環回到開頭) + +#### Random Mode / 隨機模式 +- **Daily Random / 每日隨機**: Uses date-based seed for random selection / 使用基於日期的種子進行隨機選擇 +- **Consistent / 一致性**: Same random selection throughout the day / 整天保持相同的隨機選擇 +- **Example / 範例**: Random but fixed selection per day (e.g., prompts 3, 7, 11) / 每天隨機但固定的選擇(例如:提示詞3, 7, 11) + +#### File Management / 檔案管理 + +- **Multiple Files / 多檔案**: Each `.txt` file has independent scheduling / 每個`.txt`檔案都有獨立的排程 +- **Time Seeds / 時間種子**: Stored as `{filename}_time_seed.json` in the Prompt folder(If Scheduled) / 以`{檔名}_time_seed.json`的形式儲存在Prompt資料夾中,假使是使用排程模式 +- **Auto-wrapping / 自動循環**: Never causes index errors, automatically wraps around (With Time Seed) / 永不會造成索引錯誤,自動循環,但可能會因此採樣到相同的圖片,請配合時間種子使用 +- **UTF-8 Support / UTF-8支援**: Handles international characters and languages / 支援國際字符和多種語言 + ### File Structure 檔案結構 ``` @@ -121,12 +180,15 @@ ComfyUI-ScheduledTask/ ├── __init__.py # Extension entry point 擴展入口點 ├── scheduler.py # Core scheduling logic & TimeToSeedList node 核心排程邏輯和時間種子節點 ├── web_handler.py # API endpoints API 端點 +├── Prompt/ # 提示詞檔案庫 +│ ├── Example.txt # 範例檔案 ├── web/ │ └── scheduled_task.js # Frontend interface 前端介面 ├── Workflow/ # Saved workflow files (auto-created) 保存的工作流程檔案(自動創建) │ ├── workflow1.json │ └── workflow2.json └── schedules.json # Schedule configuration (auto-created) 排程配置(自動創建) + ``` ### Workflow Format 工作流程格式 Saved workflows are stored in ComfyUI API format: diff --git a/__init__.py b/__init__.py index 43825ab..e40e845 100644 --- a/__init__.py +++ b/__init__.py @@ -4,10 +4,20 @@ ComfyUI-ScheduledTask """ import os -from .scheduler import SchedulerManager, TimeToSeedList -from .web_handler import setup_routes import threading import time +import atexit + +# 匯入節點類別和排程管理器 +from .scheduler import SchedulerManager, TimeToSeedList, DailyPromptScheduler + +# 匯入web處理器 (如果存在的話) +try: + from .web_handler import setup_routes + HAS_WEB_HANDLER = True +except ImportError: + HAS_WEB_HANDLER = False + print("Warning: web_handler not found, web interface will be disabled") # 全域排程管理器 scheduler_manager = None @@ -20,35 +30,48 @@ def get_scheduler(): # 延遲初始化 def delayed_init(): - time.sleep(3) # 等待ComfyUI完全啟動 - get_scheduler() + try: + time.sleep(3) # 等待ComfyUI完全啟動 + get_scheduler() + except Exception as e: + print(f"Error in delayed initialization: {e}") # 啟動初始化執行緒 init_thread = threading.Thread(target=delayed_init, daemon=True) init_thread.start() -# 設定web路由 -setup_routes() +# 設定web路由 (如果可用) +if HAS_WEB_HANDLER: + try: + setup_routes() + except Exception as e: + print(f"Error setting up web routes: {e}") # 清理函數 -import atexit def cleanup(): global scheduler_manager if scheduler_manager: - scheduler_manager.stop() + try: + scheduler_manager.stop() + except Exception as e: + print(f"Error during cleanup: {e}") atexit.register(cleanup) -# ComfyUI節點映射 +# 節點類別映射 - 這是關鍵! NODE_CLASS_MAPPINGS = { "TimeToSeedList": TimeToSeedList, + "DailyPromptScheduler": DailyPromptScheduler, } +# 節點顯示名稱映射 NODE_DISPLAY_NAME_MAPPINGS = { "TimeToSeedList": "Time to Seed List", + "DailyPromptScheduler": "Daily Prompt Scheduler", } -# Web擴展映射 - 這是關鍵! +# Web擴展映射 WEB_DIRECTORY = "./web" +# 確保必要的匯出 __all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', 'WEB_DIRECTORY'] \ No newline at end of file diff --git a/prompt/Example.txt b/prompt/Example.txt new file mode 100644 index 0000000..1e6e849 --- /dev/null +++ b/prompt/Example.txt @@ -0,0 +1,12 @@ +Ancient magical laboratory filled with glowing potions, floating books, crystal orbs, wooden shelves lined with mysterious bottles, candlelight illumination, mystical atmosphere, detailed fantasy art style +Wizard's underground laboratory with bubbling cauldrons, spell circles carved in stone floor, hanging herbs and ingredients, magical apparatus, dim torchlight, medieval fantasy setting +Modern witch's laboratory combining magical and scientific elements, glass beakers with swirling colorful potions, digital displays showing magical formulas, LED lighting mixed with candles, contemporary fantasy aesthetic +Alchemist's tower laboratory with spiral staircases, floating magical instruments, golden light streaming through stained glass windows, ancient tomes, transmutation circles, Renaissance-inspired magical workshop +Steampunk magical laboratory with brass pipes, mechanical magical devices, steam-powered cauldrons, copper and bronze equipment, Victorian-era magical engineering, gear-driven spell apparatus +Enchanted forest laboratory built inside a giant tree, bioluminescent plants as light sources, natural wooden surfaces, magical mushrooms, fairy lights, organic magical equipment, nature-integrated magic workshop +Crystal cave magical laboratory with towering crystal formations, prismatic light effects, levitating gemstones, magical energy coursing through crystal networks, ethereal glow, crystalline magical apparatus +Underwater magical laboratory in an air bubble, aquatic magical creatures swimming outside, coral-based equipment, bioluminescent sea plants, magical water manipulation devices, oceanic fantasy setting +Celestial magical observatory laboratory on a floating platform, star charts and astronomical instruments, magical telescopes, cosmic energy collectors, nebula views through windows, space-themed magical research facility +Miniature magical laboratory inside a snow globe, tiny detailed magical equipment, microscopic potions, doll-house scale magical apparatus, whimsical and intricate design, magical realism aesthetic +Dark gothic magical laboratory with ornate iron fixtures, dramatic shadows, purple and green magical flames, ancient grimoires bound in leather, gargoyle statues, gothic arches, ominous magical energy +Bright cheerful magical bakery laboratory, pastel-colored potions that look like cupcakes, magical mixing bowls, enchanted ovens, candy-colored magical ingredients, whimsical cartoon style, sweet magical atmosphere \ No newline at end of file diff --git a/readme/demo3.jpg b/readme/demo3.jpg new file mode 100644 index 0000000..f8561a0 Binary files /dev/null and b/readme/demo3.jpg differ diff --git a/scheduler.py b/scheduler.py index 8128b50..4cdc8e8 100644 --- a/scheduler.py +++ b/scheduler.py @@ -14,6 +14,184 @@ import random from datetime import datetime import hashlib +class DailyPromptScheduler: + def __init__(self): + self.node_dir = os.path.dirname(os.path.abspath(__file__)) + self.prompt_dir = os.path.join(self.node_dir, "Prompt") + + # Ensure Prompt folder exists + if not os.path.exists(self.prompt_dir): + os.makedirs(self.prompt_dir) + + def get_txt_files(self): + """Get all txt files in the Prompt folder""" + if not os.path.exists(self.prompt_dir): + return ["Please place txt files in Prompt folder"] + + # Only get .txt files, exclude .json files + txt_files = [f for f in os.listdir(self.prompt_dir) + if f.endswith('.txt') and not f.endswith('_time_seed.json')] + if not txt_files: + return ["Please place txt files in Prompt folder"] + + return txt_files + + def get_seed_file_path(self, txt_filename): + """Get the seed file path for specific txt file""" + # Remove .txt extension and add _time_seed.json + base_name = os.path.splitext(txt_filename)[0] + seed_filename = f"{base_name}_time_seed.json" + return os.path.join(self.prompt_dir, seed_filename) + + def load_time_seed(self, txt_filename): + """Load time seed for specific txt file""" + seed_file = self.get_seed_file_path(txt_filename) + if os.path.exists(seed_file): + try: + with open(seed_file, 'r', encoding='utf-8') as f: + seed_data = json.load(f) + return seed_data.get('seed_date', None) + except: + return None + return None + + def save_time_seed(self, txt_filename, seed_date): + """Save time seed for specific txt file""" + seed_file = self.get_seed_file_path(txt_filename) + seed_data = {'seed_date': seed_date} + with open(seed_file, 'w', encoding='utf-8') as f: + json.dump(seed_data, f, ensure_ascii=False) + + @classmethod + def INPUT_TYPES(cls): + instance = cls() + txt_files = instance.get_txt_files() + + return { + "required": { + "txt_file": (txt_files, {"default": txt_files[0] if txt_files else ""}), + "daily_count": ("INT", { + "default": 3, + "min": 1, + "max": 5000, + "step": 1 + }), + "scheduled": ("BOOLEAN", { + "default": False, + "label_on": "Set Time Seed", + "label_off": "Random Mode" + }), + } + } + + RETURN_TYPES = ("STRING", "INT") + RETURN_NAMES = ("daily_prompts", "total_index") + OUTPUT_IS_LIST = (True, False) + FUNCTION = "get_daily_prompts" + CATEGORY = "text/scheduled" + + def IS_CHANGED(self, txt_file, daily_count, scheduled): + """Ensure node is not cached""" + current_time = datetime.now() + current_date = current_time.strftime("%Y%m%d") + + # If file doesn't exist, return current timestamp + txt_path = os.path.join(self.prompt_dir, txt_file) + if not os.path.exists(txt_path): + return str(current_time.timestamp()) + + # Combine file modification time, current date and settings to generate identifier + try: + file_mtime = os.path.getmtime(txt_path) + unique_id = f"{file_mtime}_{current_date}_{daily_count}_{scheduled}_{txt_file}" + return hashlib.md5(unique_id.encode()).hexdigest() + except: + return str(current_time.timestamp()) + + def read_txt_file(self, filename): + """Read and parse txt file""" + txt_path = os.path.join(self.prompt_dir, filename) + + if not os.path.exists(txt_path): + return [] + + try: + with open(txt_path, 'r', encoding='utf-8') as f: + content = f.read() + + # Split by newlines, filter empty lines + lines = [line.strip() for line in content.split('\n') if line.strip()] + return lines + except Exception as e: + logger.error(f"Error reading file: {e}") + return [] + + def get_daily_prompts(self, txt_file, daily_count, scheduled): + # Check if file exists + if txt_file == "Please place txt files in Prompt folder": + return (["Error: Please place txt files in Prompt folder"], 0) + + # Read prompt list + all_prompts = self.read_txt_file(txt_file) + + if not all_prompts: + error_msg = f"Error: Unable to read file {txt_file} or file is empty" + return ([error_msg], 0) + + current_time = datetime.now() + current_date = current_time.strftime("%Y%m%d") + + # Ensure not exceeding available prompts count + actual_count = min(daily_count, len(all_prompts)) + + # Handle scheduling logic + if scheduled: + # Scheduled mode with time seed + seed_date = self.load_time_seed(txt_file) + + if seed_date is None: + # First time setting seed + self.save_time_seed(txt_file, current_date) + seed_date = current_date + status = f"Time seed set: {seed_date} for {txt_file}" + else: + status = f"Using time seed: {seed_date} for {txt_file}" + + # Calculate days difference from seed date + try: + seed_datetime = datetime.strptime(seed_date, "%Y%m%d") + current_datetime = datetime.strptime(current_date, "%Y%m%d") + days_diff = (current_datetime - seed_datetime).days + except: + days_diff = 0 + + # Calculate starting index for sequential selection + total_prompts = len(all_prompts) + start_index = (days_diff * actual_count) % total_prompts + + # Sequential selection with wrapping + selected_prompts = [] + for i in range(actual_count): + index = (start_index + i) % total_prompts + selected_prompts.append(all_prompts[index]) + + else: + # Random mode: use different random seed each day + random_seed = int(current_date) + hash(txt_file) + random.seed(random_seed) + status = f"Random mode - Date seed: {current_date} for {txt_file}" + + # Randomly select prompts without replacement + selected_prompts = random.sample(all_prompts, actual_count) + + # Output debug information + logger.info(f"DailyPromptScheduler: {status}") + logger.info(f"DailyPromptScheduler: Selected {actual_count} from {len(all_prompts)} prompts") + logger.info(f"DailyPromptScheduler: Selected prompts: {selected_prompts}") + + return (selected_prompts, actual_count) + + class TimeToSeedList: """ Generate random seed list based on current time diff --git a/schedules.json b/schedules.json index 2c371e3..d415846 100644 --- a/schedules.json +++ b/schedules.json @@ -1,11 +1,11 @@ { "schedules": [ { - "time": "09:09", - "workflow": "Exmaple.json", + "time": "23:22", + "workflow": "ExmapleDailyPrompt.json", "enabled": true } ], "globalEnabled": true, - "updated_at": "2025-06-19T09:07:21.402267" + "updated_at": "2025-06-20T23:20:23.382801" } \ No newline at end of file diff --git a/workflow/ExmapleDailyPrompt.json b/workflow/ExmapleDailyPrompt.json new file mode 100644 index 0000000..ee63d6a --- /dev/null +++ b/workflow/ExmapleDailyPrompt.json @@ -0,0 +1,136 @@ +{ + "3": { + "inputs": { + "seed": [ + "31", + 0 + ], + "steps": 20, + "cfg": 8, + "sampler_name": "euler", + "scheduler": "normal", + "denoise": 1, + "model": [ + "4", + 0 + ], + "positive": [ + "6", + 0 + ], + "negative": [ + "7", + 0 + ], + "latent_image": [ + "5", + 0 + ] + }, + "class_type": "KSampler", + "_meta": { + "title": "KSampler" + } + }, + "4": { + "inputs": { + "ckpt_name": "dreamshaper_8.safetensors" + }, + "class_type": "CheckpointLoaderSimple", + "_meta": { + "title": "Load Checkpoint" + } + }, + "5": { + "inputs": { + "width": 512, + "height": 512, + "batch_size": 1 + }, + "class_type": "EmptyLatentImage", + "_meta": { + "title": "Empty Latent Image" + } + }, + "6": { + "inputs": { + "text": [ + "30", + 0 + ], + "clip": [ + "4", + 1 + ] + }, + "class_type": "CLIPTextEncode", + "_meta": { + "title": "CLIP Text Encode (Prompt)" + } + }, + "7": { + "inputs": { + "text": "text, watermark", + "clip": [ + "4", + 1 + ] + }, + "class_type": "CLIPTextEncode", + "_meta": { + "title": "CLIP Text Encode (Prompt)" + } + }, + "8": { + "inputs": { + "samples": [ + "3", + 0 + ], + "vae": [ + "4", + 2 + ] + }, + "class_type": "VAEDecode", + "_meta": { + "title": "VAE Decode" + } + }, + "9": { + "inputs": { + "filename_prefix": "ComfyUI", + "images": [ + "8", + 0 + ] + }, + "class_type": "SaveImage", + "_meta": { + "title": "Save Image" + } + }, + "30": { + "inputs": { + "txt_file": "Example.txt", + "daily_count": 3, + "scheduled": false + }, + "class_type": "DailyPromptScheduler", + "_meta": { + "title": "Daily Prompt Scheduler" + } + }, + "31": { + "inputs": { + "count": [ + "30", + 1 + ] + }, + "class_type": "TimeToSeedList", + "_meta": { + "title": "Time to Seed List" + } + } +} \ No newline at end of file