diff --git a/.flake8 b/.flake8 index c9aac90..2a57acf 100644 --- a/.flake8 +++ b/.flake8 @@ -1,7 +1,7 @@ [flake8] max-line-length = 127 max-complexity = 10 -exclude = +exclude = .git, __pycache__, .mypy_cache, @@ -12,7 +12,7 @@ exclude = dist, *.egg-info, .tox -ignore = +ignore = # W503: line break before binary operator (conflicts with Black) W503, # E203: whitespace before ':' (conflicts with Black) @@ -32,4 +32,4 @@ per-file-ignores = # Statistics count = True -statistics = True \ No newline at end of file +statistics = True diff --git a/.gitattributes b/.gitattributes index c0d39dd..8a3d456 100644 --- a/.gitattributes +++ b/.gitattributes @@ -38,4 +38,4 @@ *.safetensors binary *.ckpt binary *.pt binary -*.pth binary \ No newline at end of file +*.pth binary diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 36ce860..645c171 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,4 +7,4 @@ updates: - package-ecosystem: "github-actions" directory: "/" schedule: - interval: "weekly" \ No newline at end of file + interval: "weekly" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index aa18d14..68d93d8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -81,4 +81,4 @@ exclude: | .*\.egg-info/| venv/| env/ - ) \ No newline at end of file + ) diff --git a/README.md b/README.md index 891ed9f..62e36df 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ Iโm sharing them here with the community, and I hope you find them as useful a | [๐งน Kiko Purge VRAM](#-kiko-purge-vram) | Intelligent VRAM management with detailed reporting | ๐ ๏ธ Utils | | [๐ Local Image Loader](#-local-image-loader) | Visual gallery browser for local media files | ๐พ Images | | [๐ Model Downloader](#-model-downloader) | Download models from CivitAI, HuggingFace, and custom URLs | ๐ ๏ธ Utils | +| [โฑ๏ธ Workflow Timer](#๏ธ-workflow-timer) | Real-time execution timer with customizable display | ๐ ๏ธ Utils | ### ๐งฐ xyz-helpers Tools @@ -405,6 +406,26 @@ Download models, LoRAs, and other assets directly from CivitAI, HuggingFace, and  +#### โฑ๏ธ Workflow Timer +Real-time execution timer that displays workflow duration with millisecond precision. + +- **Live Timing**: Updates in real-time during workflow execution (MM:SS:mmm format) +- **Customizable Color**: Choose your preferred display color via KikoTools settings +- **Glow Effect**: Optional pulsing glow animation (can be enabled/disabled in settings) +- **Global Settings**: Color and glow preferences apply to all timer nodes +- **Persistent Display**: Shows final execution time after workflow completes +- **Multi-Node Sync**: All timer nodes stay synchronized during execution + +**Use Cases:** +- Monitor workflow execution performance +- Compare generation times across different settings +- Identify slow nodes by adding timers at different workflow stages +- Track optimization improvements over time + +**Settings (KikoTools Settings Panel):** +- **Workflow Timer: Color** - Custom color picker for timer display +- **Workflow Timer: Enable Glow** - Toggle pulsing glow effect on/off + ### ๐ค Embedding Autocomplete **Intelligent autocomplete for embeddings, LoRAs, and custom tags in text prompts.** @@ -448,7 +469,7 @@ This feature is an enhanced fork of the autocomplete functionality from [ComfyUI **Intelligent GPU memory management with threshold-based triggering and detailed reporting.** **Key Features:** -- **4 Purge Modes**: +- **4 Purge Modes**: - `soft`: Basic garbage collection and cache clearing - `aggressive`: Multiple GC passes with full CUDA cache clearing - `models_only`: Unload all models and clear model cache @@ -728,6 +749,7 @@ Example workflow available: [xyz_helpers_lora_testing.json](examples/workflows/x | **Text Encode Sampler Params** | Combined text encoding and parameter management | โ Complete | [Docs](examples/documentation/text_encode_sampler_params.md) | | **Local Image Loader** | Visual gallery browser for local media files | โ Complete | [Docs](examples/documentation/local_image_loader.md) | | **Model Downloader** | Download models from CivitAI, HuggingFace, and custom URLs | โ Complete | [Docs](examples/documentation/model_downloader.md) | +| **Workflow Timer** | Real-time execution timer with customizable display | โ Complete | [Docs](examples/documentation/workflow_timer.md) | | **Batch Image Processor** | Process multiple images with consistent settings | ๐ง Planned | Coming Soon | | **Advanced Prompt Utilities** | Enhanced prompt manipulation and generation | ๐ง Planned | Coming Soon | @@ -1021,7 +1043,7 @@ MIT License - see [LICENSE](LICENSE) file for details. ## ๐ Stats -- **Nodes**: 20 (14 core tools + 6 xyz-helpers) +- **Nodes**: 21 (15 core tools + 6 xyz-helpers) - **Features**: Embedding Autocomplete (settings-based, not a node) - **Categories**: 9 emoji-based categories for better organization - **Download Platforms**: 3 (CivitAI, HuggingFace, Custom URLs) diff --git a/examples/documentation/batch_prompts.md b/examples/documentation/batch_prompts.md index 9d6af1f..7caa108 100644 --- a/examples/documentation/batch_prompts.md +++ b/examples/documentation/batch_prompts.md @@ -103,4 +103,4 @@ The node provides several ways to track progress: ### State persistence - State is stored in your system's temp directory - Clear `/tmp/comfyui_batch_prompts/` to reset all counters -- Use `reload_file` to reset counter for a specific file \ No newline at end of file +- Use `reload_file` to reset counter for a specific file diff --git a/examples/documentation/display_any.md b/examples/documentation/display_any.md index 14441bd..b94fff0 100644 --- a/examples/documentation/display_any.md +++ b/examples/documentation/display_any.md @@ -117,4 +117,4 @@ Config Node โ Display Any (raw value) โ Processing Node [Text Multiline] โ [Concatenate] โ "Image dimensions: " ``` -This creates a text output showing the current image dimensions that can be used elsewhere in your workflow. \ No newline at end of file +This creates a text output showing the current image dimensions that can be used elsewhere in your workflow. diff --git a/examples/documentation/display_text.md b/examples/documentation/display_text.md index fa3e7c5..eb4c957 100644 --- a/examples/documentation/display_text.md +++ b/examples/documentation/display_text.md @@ -85,7 +85,7 @@ Display long text content with scrolling and word wrapping. The node intelligently detects prompt formats: -1. **SDXL Format**: +1. **SDXL Format**: - Looks for "Positive prompt:" and "Negative prompt:" markers - Case-insensitive detection - Handles various formatting styles @@ -98,7 +98,7 @@ The node intelligently detects prompt formats: ## Styling - **Font**: Monospace for consistent alignment -- **Colors**: +- **Colors**: - Text: Light gray (#ddd) on dark background - Background: Semi-transparent dark (#1a1a1a) - Borders: Subtle gray (#333) @@ -144,4 +144,4 @@ The node intelligently detects prompt formats: SDXL Format Split View Display Clean Prompts ``` -This creates a seamless workflow from prompt generation to usage, with the Display Text node providing the visual interface for review and interaction. \ No newline at end of file +This creates a seamless workflow from prompt generation to usage, with the Display Text node providing the visual interface for review and interaction. diff --git a/examples/documentation/flux_sampler_params.md b/examples/documentation/flux_sampler_params.md index 4b64e9c..800373d 100644 --- a/examples/documentation/flux_sampler_params.md +++ b/examples/documentation/flux_sampler_params.md @@ -149,4 +149,4 @@ base_shift: 0.4 - **1.0.3**: Improved UI elements and parameter validation ## Credits -Original implementation by cubiq in [comfyui-essentials-nodes](https://github.com/cubiq/ComfyUI_essentials). Adapted and maintained by the ComfyAssets team. \ No newline at end of file +Original implementation by cubiq in [comfyui-essentials-nodes](https://github.com/cubiq/ComfyUI_essentials). Adapted and maintained by the ComfyAssets team. diff --git a/examples/documentation/gemini_prompt.md b/examples/documentation/gemini_prompt.md index 60655c4..551f946 100644 --- a/examples/documentation/gemini_prompt.md +++ b/examples/documentation/gemini_prompt.md @@ -206,4 +206,4 @@ Errors are displayed in the prompt output for easy debugging. **Import error for google-generativeai**: - Run `pip install google-generativeai` in your ComfyUI environment -- Restart ComfyUI after installation \ No newline at end of file +- Restart ComfyUI after installation diff --git a/examples/documentation/image_to_multiple_of.md b/examples/documentation/image_to_multiple_of.md index 4a7539b..9921715 100644 --- a/examples/documentation/image_to_multiple_of.md +++ b/examples/documentation/image_to_multiple_of.md @@ -79,4 +79,4 @@ Load Images โ Image to Multiple Of (multiple_of: 16, method: rescale) โ Batc The node will raise an error if: - The image dimensions are smaller than the specified multiple_of value - Invalid input types are provided -- The resulting dimensions would be 0 or negative \ No newline at end of file +- The resulting dimensions would be 0 or negative diff --git a/examples/documentation/kiko_film_grain.md b/examples/documentation/kiko_film_grain.md index 35c285a..5f058af 100644 --- a/examples/documentation/kiko_film_grain.md +++ b/examples/documentation/kiko_film_grain.md @@ -122,4 +122,4 @@ Creates monochrome grain perfect for black and white photography. - Works with any image format supported by ComfyUI - Preserves image properties (alpha channel, batch size) - Compatible with both RGB and RGBA images -- Efficient batch processing support \ No newline at end of file +- Efficient batch processing support diff --git a/examples/documentation/kiko_save_image.md b/examples/documentation/kiko_save_image.md index 22372ee..1726732 100644 --- a/examples/documentation/kiko_save_image.md +++ b/examples/documentation/kiko_save_image.md @@ -50,7 +50,7 @@ Enhanced image saving node with multiple format support, quality controls, and a ### Image Grid - **Thumbnails**: Click any image to open full-size in new tab - **File Info**: Shows filename and size for each image -- **Quality Indicators**: +- **Quality Indicators**: - PNG: Compression level (0-9) - JPEG/WebP: Quality percentage - **Batch Selection**: Checkboxes for multi-select operations @@ -210,4 +210,4 @@ Batch Generate โ Kiko Save Image โ Popup Viewer **Can't see all images**: - Scroll within the popup grid - Maximize the popup window -- Images are shown newest first \ No newline at end of file +- Images are shown newest first diff --git a/examples/documentation/local_image_loader.md b/examples/documentation/local_image_loader.md index 6f27c66..befb56e 100644 --- a/examples/documentation/local_image_loader.md +++ b/examples/documentation/local_image_loader.md @@ -155,4 +155,4 @@ The node creates a visual widget that runs in the ComfyUI interface and communic - Save user preferences - Handle file selection -All file operations are performed server-side for security, with proper path validation to prevent directory traversal attacks. \ No newline at end of file +All file operations are performed server-side for security, with proper path validation to prevent directory traversal attacks. diff --git a/examples/documentation/lora_folder_batch.md b/examples/documentation/lora_folder_batch.md index fa1799f..df17f9f 100644 --- a/examples/documentation/lora_folder_batch.md +++ b/examples/documentation/lora_folder_batch.md @@ -261,4 +261,4 @@ batch_mode: sequential - **1.0.4**: Added auto-batching for large LoRA collections ## Credits -Original implementation by cubiq in [comfyui-essentials-nodes](https://github.com/cubiq/ComfyUI_essentials). Adapted and maintained by the ComfyAssets team. \ No newline at end of file +Original implementation by cubiq in [comfyui-essentials-nodes](https://github.com/cubiq/ComfyUI_essentials). Adapted and maintained by the ComfyAssets team. diff --git a/examples/documentation/plot_parameters.md b/examples/documentation/plot_parameters.md index 5fa3996..60e8d19 100644 --- a/examples/documentation/plot_parameters.md +++ b/examples/documentation/plot_parameters.md @@ -109,12 +109,12 @@ Parameter Grid โ PlotParameters โ Analysis Display x_axis: "guidance" y_axis: "perceived_quality" -# Step efficiency analysis +# Step efficiency analysis x_axis: "steps" y_axis: "generation_time" # LoRA impact assessment -x_axis: "lora_strength" +x_axis: "lora_strength" y_axis: "style_adherence" ``` @@ -231,4 +231,4 @@ Compare multiple generation runs to identify optimal parameters. - **1.0.3**: Improved export capabilities ## Credits -Original implementation by cubiq in [comfyui-essentials-nodes](https://github.com/cubiq/ComfyUI_essentials). Adapted and maintained by the ComfyAssets team. \ No newline at end of file +Original implementation by cubiq in [comfyui-essentials-nodes](https://github.com/cubiq/ComfyUI_essentials). Adapted and maintained by the ComfyAssets team. diff --git a/examples/documentation/sampler_select_helper.md b/examples/documentation/sampler_select_helper.md index c1d0f77..27b195f 100644 --- a/examples/documentation/sampler_select_helper.md +++ b/examples/documentation/sampler_select_helper.md @@ -257,4 +257,4 @@ Compare all compatible samplers for specific model/prompt combination. - **1.0.3**: Improved auto-detection ## Credits -Original implementation by cubiq in [comfyui-essentials-nodes](https://github.com/cubiq/ComfyUI_essentials). Adapted and maintained by the ComfyAssets team. \ No newline at end of file +Original implementation by cubiq in [comfyui-essentials-nodes](https://github.com/cubiq/ComfyUI_essentials). Adapted and maintained by the ComfyAssets team. diff --git a/examples/documentation/scheduler_select_helper.md b/examples/documentation/scheduler_select_helper.md index 3f9735c..40a4ab8 100644 --- a/examples/documentation/scheduler_select_helper.md +++ b/examples/documentation/scheduler_select_helper.md @@ -297,4 +297,4 @@ Progress through schedulers from fast to quality for different use cases. - **1.0.3**: Improved compatibility matrix ## Credits -Original implementation by cubiq in [comfyui-essentials-nodes](https://github.com/cubiq/ComfyUI_essentials). Adapted and maintained by the ComfyAssets team. \ No newline at end of file +Original implementation by cubiq in [comfyui-essentials-nodes](https://github.com/cubiq/ComfyUI_essentials). Adapted and maintained by the ComfyAssets team. diff --git a/examples/documentation/text_encode_sampler_params.md b/examples/documentation/text_encode_sampler_params.md index 485d07b..6cecf35 100644 --- a/examples/documentation/text_encode_sampler_params.md +++ b/examples/documentation/text_encode_sampler_params.md @@ -142,7 +142,7 @@ steps: 30-40 cfg: 7-8 sampler: dpmpp_3m_sde -# Speed over quality +# Speed over quality steps: 10-15 cfg: 5-6 sampler: euler @@ -307,4 +307,4 @@ Very High (50+): Diminishing returns - **1.0.3**: Improved batch processing ## Credits -Original implementation by cubiq in [comfyui-essentials-nodes](https://github.com/cubiq/ComfyUI_essentials). Adapted and maintained by the ComfyAssets team. \ No newline at end of file +Original implementation by cubiq in [comfyui-essentials-nodes](https://github.com/cubiq/ComfyUI_essentials). Adapted and maintained by the ComfyAssets team. diff --git a/examples/test_prompts.txt b/examples/test_prompts.txt index 17f5820..e49fbad 100644 --- a/examples/test_prompts.txt +++ b/examples/test_prompts.txt @@ -11,4 +11,4 @@ Enchanted forest with magical glowing mushrooms, fairy lights, mystical atmosphe Negative: desert, urban, modern, realistic, mundane --- Space station orbiting Earth, detailed mechanical structures, astronauts performing spacewalk, realistic sci-fi, NASA photography -Negative: fantasy, medieval, underwater, cartoon style \ No newline at end of file +Negative: fantasy, medieval, underwater, cartoon style diff --git a/examples/workflows/display_any_example.json b/examples/workflows/display_any_example.json index f1c4a8f..165511b 100644 --- a/examples/workflows/display_any_example.json +++ b/examples/workflows/display_any_example.json @@ -376,4 +376,4 @@ "VHS_KeepIntermediate": true }, "version": 0.4 -} \ No newline at end of file +} diff --git a/examples/workflows/display_text_example.json b/examples/workflows/display_text_example.json index 01823e5..3fd07f8 100644 --- a/examples/workflows/display_text_example.json +++ b/examples/workflows/display_text_example.json @@ -141,4 +141,4 @@ "VHS_KeepIntermediate": true }, "version": 0.4 -} \ No newline at end of file +} diff --git a/examples/workflows/empty_latent_batch_example.json b/examples/workflows/empty_latent_batch_example.json index d476bb3..001071c 100644 --- a/examples/workflows/empty_latent_batch_example.json +++ b/examples/workflows/empty_latent_batch_example.json @@ -298,4 +298,4 @@ "VHS_KeepIntermediate": true }, "version": 0.4 -} \ No newline at end of file +} diff --git a/examples/workflows/gemini_prompt_example.json b/examples/workflows/gemini_prompt_example.json index afc3e21..5740052 100644 --- a/examples/workflows/gemini_prompt_example.json +++ b/examples/workflows/gemini_prompt_example.json @@ -204,4 +204,4 @@ "VHS_KeepIntermediate": true }, "version": 0.4 -} \ No newline at end of file +} diff --git a/examples/workflows/image_to_multiple_of_example.json b/examples/workflows/image_to_multiple_of_example.json index 7b14a84..1801fd6 100644 --- a/examples/workflows/image_to_multiple_of_example.json +++ b/examples/workflows/image_to_multiple_of_example.json @@ -549,4 +549,4 @@ "VHS_KeepIntermediate": true }, "version": 0.4 -} \ No newline at end of file +} diff --git a/examples/workflows/kiko_film_grain_example.json b/examples/workflows/kiko_film_grain_example.json index 74dbcdd..35e0ce7 100644 --- a/examples/workflows/kiko_film_grain_example.json +++ b/examples/workflows/kiko_film_grain_example.json @@ -162,4 +162,4 @@ } }, "version": 0.4 -} \ No newline at end of file +} diff --git a/examples/workflows/kiko_save_image_example.json b/examples/workflows/kiko_save_image_example.json index db7d335..2b2f308 100644 --- a/examples/workflows/kiko_save_image_example.json +++ b/examples/workflows/kiko_save_image_example.json @@ -144,4 +144,4 @@ "VHS_KeepIntermediate": true }, "version": 0.4 -} \ No newline at end of file +} diff --git a/examples/workflows/resolution_calculator_example.json b/examples/workflows/resolution_calculator_example.json index 08e50fa..2171619 100644 --- a/examples/workflows/resolution_calculator_example.json +++ b/examples/workflows/resolution_calculator_example.json @@ -352,4 +352,4 @@ "VHS_KeepIntermediate": true }, "version": 0.4 -} \ No newline at end of file +} diff --git a/examples/workflows/xyz_helpers_sampler_comparison.json b/examples/workflows/xyz_helpers_sampler_comparison.json index f2eeb6e..4551ecc 100644 --- a/examples/workflows/xyz_helpers_sampler_comparison.json +++ b/examples/workflows/xyz_helpers_sampler_comparison.json @@ -166,4 +166,4 @@ ], "attribution": "xyz_helpers nodes adapted from comfyui-essentials-nodes" } -} \ No newline at end of file +} diff --git a/kikotools/__init__.py b/kikotools/__init__.py index abfed77..df52781 100644 --- a/kikotools/__init__.py +++ b/kikotools/__init__.py @@ -13,6 +13,7 @@ from .tools.image_scale_down_by import ImageScaleDownByNode from .tools.image_to_multiple_of import ImageToMultipleOfNode from .tools.kiko_film_grain import KikoFilmGrainNode from .tools.kiko_purge_vram import KikoPurgeVRAM +from .tools.kiko_workflow_timer import KikoWorkflowTimerNode from .tools.kiko_save_image import KikoSaveImageNode from .tools.local_image_loader import LocalImageLoaderNode from .tools.model_downloader import ModelDownloaderNode @@ -21,6 +22,7 @@ from .tools.sampler_combo import SamplerComboCompactNode, SamplerComboNode from .tools.seed_history import SeedHistoryNode from .tools.text_input import TextInputNode from .tools.width_height_selector import WidthHeightSelectorNode +from .tools.width_height_to_vec2 import WidthHeightToVec2Node from .tools.xyz_helpers import ( FluxSamplerParamsNode, LoRAFolderBatchNode, @@ -56,6 +58,8 @@ NODE_CLASS_MAPPINGS = { "FluxSamplerParams": FluxSamplerParamsNode, "PlotParameters+": PlotParametersNode, "LoRAFolderBatch": LoRAFolderBatchNode, + "WidthHeightToVec2": WidthHeightToVec2Node, + "KikoWorkflowTimer": KikoWorkflowTimerNode, # Note: KikoEmbeddingAutocomplete is not registered as a node # It's a settings-only feature accessed through ComfyUI settings menu } @@ -85,6 +89,8 @@ NODE_DISPLAY_NAME_MAPPINGS = { "FluxSamplerParams": "Flux Sampler Parameters", "PlotParameters+": "Plot Parameters", "LoRAFolderBatch": "LoRA Folder Batch", + "WidthHeightToVec2": "Width Height to VEC2", + "KikoWorkflowTimer": "Workflow Timer", # KikoEmbeddingAutocomplete removed - settings only, not a node } diff --git a/kikotools/tools/gemini_prompt/.gemini_models_cache.json b/kikotools/tools/gemini_prompt/.gemini_models_cache.json index 4fcfcdc..9e94ff9 100644 --- a/kikotools/tools/gemini_prompt/.gemini_models_cache.json +++ b/kikotools/tools/gemini_prompt/.gemini_models_cache.json @@ -86,4 +86,4 @@ "gemini-2.5-flash-lite": "Gemini 2.5 Flash-Lite" }, "timestamp": 1754568195.1098156 -} \ No newline at end of file +} diff --git a/kikotools/tools/kiko_workflow_timer/__init__.py b/kikotools/tools/kiko_workflow_timer/__init__.py new file mode 100644 index 0000000..fd81c54 --- /dev/null +++ b/kikotools/tools/kiko_workflow_timer/__init__.py @@ -0,0 +1,10 @@ +""" +KikoWorkflow Timer - Display real-time execution timer for ComfyUI workflows. + +Provides a visual timer that tracks workflow execution duration with +millisecond precision. +""" + +from .node import KikoWorkflowTimerNode + +__all__ = ["KikoWorkflowTimerNode"] diff --git a/kikotools/tools/kiko_workflow_timer/node.py b/kikotools/tools/kiko_workflow_timer/node.py new file mode 100644 index 0000000..40e1119 --- /dev/null +++ b/kikotools/tools/kiko_workflow_timer/node.py @@ -0,0 +1,52 @@ +""" +KikoWorkflow Timer Node + +A display-only node that shows real-time execution timing for ComfyUI workflows. +The timer is managed entirely on the frontend via WebSocket events. +""" + +from ...base import ComfyAssetsBaseNode + + +class KikoWorkflowTimerNode(ComfyAssetsBaseNode): + """ + A UI node that displays a real-time timer for workflow execution. + + The timer starts when execution begins and stops when the workflow + completes, showing the total elapsed time in MM:SS:mmm format. + + This is a display-only node with no inputs or outputs - all timing + logic is handled by the JavaScript frontend via WebSocket events. + """ + + DISPLAY_NAME = "Workflow Timer" + CATEGORY = "๐ซถ ComfyAssets/๐ ๏ธ Utils" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": {}, + "hidden": { + "prompt": "PROMPT", + "unique_id": "UNIQUE_ID", + }, + } + + RETURN_TYPES = () + FUNCTION = "execute" + OUTPUT_NODE = True + + def execute(self, **kwargs): + """ + Execute method - returns empty since this is a display-only node. + + The actual timer functionality is handled entirely by the JavaScript + frontend which hooks into ComfyUI's WebSocket events. + + Args: + **kwargs: Hidden parameters (prompt, unique_id) + + Returns: + Empty dict - no outputs + """ + return {} diff --git a/kikotools/tools/local_image_loader/config.json b/kikotools/tools/local_image_loader/config.json index e9b712b..d06b7b7 100644 --- a/kikotools/tools/local_image_loader/config.json +++ b/kikotools/tools/local_image_loader/config.json @@ -1,7 +1,12 @@ { - "last_path": "/home/vito/Downloads/vito", + "last_path": "/home/vito/ai-apps/ComfyUI/output/vids/images", "saved_paths": [ "/home/vito/ai-apps/ComfyUI-3.12/output/2025-05-01", - "/home/vito/ai-apps/ComfyUI-3.12/output/" + "/home/vito/ai-apps/ComfyUI-3.12/output/", + "/home/vito/Downloads/vito", + "/home/vito/ai-apps/ComfyUI/output/2025-06-08", + "/home/vito/ai-apps/ComfyUI/output", + "/home/vito/Downloads", + "/home/vito/Downloads/images" ] } \ No newline at end of file diff --git a/kikotools/tools/local_image_loader/logic.py b/kikotools/tools/local_image_loader/logic.py index 95716aa..96d1220 100644 --- a/kikotools/tools/local_image_loader/logic.py +++ b/kikotools/tools/local_image_loader/logic.py @@ -73,6 +73,7 @@ def scan_directory( show_audio: bool = False, sort_by: str = "name", sort_order: str = "asc", + hide_dot_folders: bool = True, ) -> List[Dict[str, Any]]: """ Scan a directory for supported media files. @@ -83,6 +84,7 @@ def scan_directory( show_audio: Include audio files sort_by: Sort criteria ('name', 'date', 'size') sort_order: Sort order ('asc', 'desc') + hide_dot_folders: Hide folders starting with a dot Returns: List of file information dictionaries @@ -94,6 +96,10 @@ def scan_directory( items = [] for item in os.listdir(directory): + # Skip dot folders/files if hide_dot_folders is enabled + if hide_dot_folders and item.startswith("."): + continue + full_path = os.path.join(directory, item) try: diff --git a/kikotools/tools/local_image_loader/node.py b/kikotools/tools/local_image_loader/node.py index 814c2c7..90ebedf 100644 --- a/kikotools/tools/local_image_loader/node.py +++ b/kikotools/tools/local_image_loader/node.py @@ -68,13 +68,9 @@ class LocalImageLoaderNode(ComfyAssetsBaseNode): RETURN_TYPES = ( "IMAGE", "STRING", - "STRING", - "STRING", ) RETURN_NAMES = ( "image", - "video_path", - "audio_path", "info", ) FUNCTION = "load_media" @@ -87,7 +83,7 @@ class LocalImageLoaderNode(ComfyAssetsBaseNode): return os.path.getmtime(SELECTIONS_FILE) return float("inf") - def load_media(self, unique_id: str) -> Tuple[torch.Tensor, str, str, str]: + def load_media(self, unique_id: str) -> Tuple[torch.Tensor, str]: """ Load selected media based on node's unique ID. @@ -95,11 +91,9 @@ class LocalImageLoaderNode(ComfyAssetsBaseNode): unique_id: Unique identifier for this node instance Returns: - Tuple of (image tensor, video path, audio path, info string) + Tuple of (image tensor, info string) """ image_tensor = create_empty_tensor() - video_path = "" - audio_path = "" info_string = "" selections = load_selections() @@ -116,19 +110,7 @@ class LocalImageLoaderNode(ComfyAssetsBaseNode): except Exception as e: print(f"KikoLocalImageLoader: Error loading image: {e}") - # Get video path if selected - video_selection = node_selections.get("video") - if video_selection and video_selection.get("path"): - if os.path.exists(video_selection["path"]): - video_path = video_selection["path"] - - # Get audio path if selected - audio_selection = node_selections.get("audio") - if audio_selection and audio_selection.get("path"): - if os.path.exists(audio_selection["path"]): - audio_path = audio_selection["path"] - - return (image_tensor, video_path, audio_path, info_string) + return (image_tensor, info_string) # Setup API routes @@ -194,6 +176,9 @@ try: if not directory or not os.path.isdir(directory): return web.json_response({"error": "Directory not found."}, status=404) + # Normalize path to remove trailing slashes and resolve relative paths + directory = os.path.normpath(directory) + # Save last path config = load_config() config["last_path"] = directory @@ -201,6 +186,9 @@ try: show_videos = request.query.get("show_videos", "false").lower() == "true" show_audio = request.query.get("show_audio", "false").lower() == "true" + hide_dot_folders = ( + request.query.get("hide_dot_folders", "true").lower() == "true" + ) page = int(request.query.get("page", 1)) per_page = int(request.query.get("per_page", 50)) @@ -209,7 +197,12 @@ try: try: items = scan_directory( - directory, show_videos, show_audio, sort_by, sort_order + directory, + show_videos, + show_audio, + sort_by, + sort_order, + hide_dot_folders, ) # Get parent directory @@ -239,6 +232,86 @@ try: """API endpoint to get last used directory path.""" return web.json_response({"last_path": load_config().get("last_path", "")}) + @prompt_server.routes.get("/kiko_local_image_loader/list_directories") + async def list_directories(request): + """API endpoint to list directories for autocomplete.""" + path = request.query.get("path", "") + + try: + # Handle empty path - show root or common starting points + if not path: + # Return filesystem root + if os.name == "nt": # Windows + import string + + drives = [ + f"{d}:\\" + for d in string.ascii_uppercase + if os.path.exists(f"{d}:\\") + ] + return web.json_response({"directories": drives}) + else: # Unix/Linux/Mac + return web.json_response({"directories": ["/"]}) + + # Normalize the path + path = os.path.expanduser(path) # Handle ~ for home directory + + # If path ends with separator, list contents of that directory + if path.endswith(os.sep) or (os.name == "nt" and path.endswith("/")): + if os.path.isdir(path): + try: + entries = os.listdir(path) + dirs = [] + for entry in entries: + full_path = os.path.join(path, entry) + if os.path.isdir(full_path): + dirs.append(full_path) + dirs.sort(key=lambda x: x.lower()) + return web.json_response( + {"directories": dirs[:50]} + ) # Limit results + except PermissionError: + return web.json_response( + {"directories": [], "error": "Permission denied"} + ) + else: + return web.json_response({"directories": []}) + + # Otherwise, find matching directories in parent + parent_dir = os.path.dirname(path) + basename = os.path.basename(path).lower() + + if not parent_dir: + # Handle root level on Unix + if path.startswith("/"): + parent_dir = "/" + basename = path[1:].lower() + else: + return web.json_response({"directories": []}) + + if os.path.isdir(parent_dir): + try: + entries = os.listdir(parent_dir) + dirs = [] + for entry in entries: + full_path = os.path.join(parent_dir, entry) + if os.path.isdir(full_path) and entry.lower().startswith( + basename + ): + dirs.append(full_path) + dirs.sort(key=lambda x: x.lower()) + return web.json_response( + {"directories": dirs[:50]} + ) # Limit results + except PermissionError: + return web.json_response( + {"directories": [], "error": "Permission denied"} + ) + + return web.json_response({"directories": []}) + except Exception as e: + return web.json_response({"directories": [], "error": str(e)}) + @prompt_server.routes.get("/kiko_local_image_loader/thumbnail") async def get_thumbnail(request): """API endpoint to get image thumbnail.""" diff --git a/kikotools/tools/local_image_loader/selections.json b/kikotools/tools/local_image_loader/selections.json index 852e172..d27dcf4 100644 --- a/kikotools/tools/local_image_loader/selections.json +++ b/kikotools/tools/local_image_loader/selections.json @@ -18,5 +18,60 @@ "image": { "path": "/home/vito/ai-apps/ComfyUI/output/ComfyUI_00002_.png" } + }, + "23": { + "image": { + "path": "/home/vito/ai-apps/ComfyUI/output/Z_Image_Char/Image_00098_.png" + } + }, + "69": { + "image": { + "path": "/home/vito/Downloads/KikoSave_00086.png" + } + }, + "52": { + "image": { + "path": "/home/vito/ai-apps/ComfyUI/output/kiko/XXX/images/kiko_00038_.png" + } + }, + "38": { + "image": { + "path": "/home/vito/Downloads/vito/IMG_20160422_163419.jpg" + } + }, + "170": { + "image": { + "path": "/home/vito/ai-apps/ComfyUI/output/Z_Image_Char/Image_00326_.png" + } + }, + "214": { + "image": { + "path": "/home/vito/ai-apps/ComfyUI/output/Z_Image_Char/Image_00329_.png" + } + }, + "299": { + "image": { + "path": "/home/vito/Downloads/images/KikoSave_00016.png" + } + }, + "527": { + "image": { + "path": "/home/vito/Downloads/ComfyUI_temp_sktzg_00012_.png" + } + }, + "522": { + "image": { + "path": "/home/vito/Downloads/KikoSave_00086.png" + } + }, + "517": { + "image": { + "path": "/home/vito/ai-apps/ComfyUI/output/vids/images/kittybear_00004.png" + } + }, + "144": { + "image": { + "path": "/home/vito/ai-apps/ComfyUI/output/2025-04-24/ComfyUI_00002_.png" + } } } \ No newline at end of file diff --git a/kikotools/tools/width_height_selector/presets.py b/kikotools/tools/width_height_selector/presets.py index af4d082..388e5d8 100644 --- a/kikotools/tools/width_height_selector/presets.py +++ b/kikotools/tools/width_height_selector/presets.py @@ -98,6 +98,16 @@ PRESET_METADATA: Dict[str, PresetMetadata] = { "Portrait", "SDXL near-square portrait - subtle portrait", ), + "720ร1280": PresetMetadata( + 720, + 1280, + "9:16", + 0.5625, + 0.92, + "SDXL", + "Portrait", + "SDXL portrait 9:16 - vertical video/mobile", + ), # SDXL Presets - Landscape "1024ร960": PresetMetadata( 1024, @@ -159,6 +169,16 @@ PRESET_METADATA: Dict[str, PresetMetadata] = { "Landscape", "SDXL landscape 3:1 - extreme wide panoramic", ), + "1280ร720": PresetMetadata( + 1280, + 720, + "16:9", + 1.778, + 0.92, + "SDXL", + "Landscape", + "SDXL landscape 16:9 - HD widescreen video", + ), # FLUX Presets - High Quality "1920ร1080": PresetMetadata( 1920, diff --git a/kikotools/tools/width_height_to_vec2/__init__.py b/kikotools/tools/width_height_to_vec2/__init__.py new file mode 100644 index 0000000..165cf91 --- /dev/null +++ b/kikotools/tools/width_height_to_vec2/__init__.py @@ -0,0 +1,8 @@ +""" +Width Height to VEC2 converter node +Converts width and height inputs to VEC2 tuple for jovi_glsl and similar nodes +""" + +from .node import WidthHeightToVec2Node + +__all__ = ["WidthHeightToVec2Node"] diff --git a/kikotools/tools/width_height_to_vec2/node.py b/kikotools/tools/width_height_to_vec2/node.py new file mode 100644 index 0000000..bdc5f3d --- /dev/null +++ b/kikotools/tools/width_height_to_vec2/node.py @@ -0,0 +1,128 @@ +""" +Width Height to VEC2 Node + +Converts width and height inputs to a VEC2 tuple for use with +nodes like jovi_glsl that expect vector inputs. +""" + +from typing import Any, Dict, Tuple, Union + +from ...base import ComfyAssetsBaseNode + + +class WidthHeightToVec2Node(ComfyAssetsBaseNode): + """ + Convert width and height values to VEC2 format. + + Accepts INT, FLOAT, STRING, or ANY types and outputs a VEC2 tuple + suitable for nodes expecting vector inputs. + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """Define ComfyUI input interface.""" + return { + "required": { + "width": ( + "INT", + { + "default": 1024, + "min": 1, + "max": 8192, + "step": 1, + "tooltip": "Width value (x component of VEC2)", + }, + ), + "height": ( + "INT", + { + "default": 1024, + "min": 1, + "max": 8192, + "step": 1, + "tooltip": "Height value (y component of VEC2)", + }, + ), + }, + } + + RETURN_TYPES = ("VEC2",) + RETURN_NAMES = ("vec2",) + FUNCTION = "convert_to_vec2" + CATEGORY = "๐ซถ ComfyAssets/๐ผ๏ธ Resolution" + + def convert_to_vec2( + self, + width: Union[int, float, str, Any], + height: Union[int, float, str, Any], + ) -> Tuple[Tuple[int, int]]: + """ + Convert width and height to VEC2 tuple. + + Args: + width: Width value (will be converted to int) + height: Height value (will be converted to int) + + Returns: + Tuple containing the VEC2 tuple (width, height) + """ + try: + # Convert to integers, handling various input types + w = self._to_int(width, "width") + h = self._to_int(height, "height") + + # Clamp values to valid range + w = max(1, min(8192, w)) + h = max(1, min(8192, h)) + + self.log_info(f"Converted to VEC2: ({w}, {h})") + + # Return as tuple wrapped in tuple (ComfyUI return format) + return ((w, h),) + + except Exception as e: + error_msg = f"Failed to convert to VEC2: {str(e)}" + self.handle_error(error_msg, e) + + def _to_int(self, value: Any, name: str) -> int: + """ + Convert a value to integer. + + Args: + value: Value to convert (int, float, str, or any) + name: Parameter name for error messages + + Returns: + Integer value + + Raises: + ValueError: If conversion fails + """ + if isinstance(value, int): + return value + elif isinstance(value, float): + return int(value) + elif isinstance(value, str): + try: + # Try parsing as float first (handles "1024.0") + return int(float(value.strip())) + except ValueError: + raise ValueError(f"Cannot convert {name} string '{value}' to integer") + else: + # Try generic conversion for ANY type + try: + return int(value) + except (ValueError, TypeError): + raise ValueError( + f"Cannot convert {name} of type {type(value).__name__} to integer" + ) + + +# Node registration +NODE_CLASS_MAPPINGS = { + "WidthHeightToVec2": WidthHeightToVec2Node, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "WidthHeightToVec2": "Width Height to VEC2", +} diff --git a/pyproject.toml b/pyproject.toml index ba8a6f0..0069524 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "kikotools" description = "Simple tools for ComfyUI" -version = "1.0.26" +version = "1.0.28" license = {text = "MIT"} dependencies = [] diff --git a/tests/unit/tools/test_kiko_workflow_timer.py b/tests/unit/tools/test_kiko_workflow_timer.py new file mode 100644 index 0000000..36094f3 --- /dev/null +++ b/tests/unit/tools/test_kiko_workflow_timer.py @@ -0,0 +1,110 @@ +""" +Unit tests for KikoWorkflowTimerNode. + +Since this is a display-only node with all logic handled by JavaScript, +these tests focus on validating the node's structure and ComfyUI integration. +""" + + +class TestKikoWorkflowTimerNode: + """Test suite for KikoWorkflowTimerNode.""" + + def test_node_import(self): + """Test that the node can be imported successfully.""" + from kikotools.tools.kiko_workflow_timer.node import KikoWorkflowTimerNode + + assert KikoWorkflowTimerNode is not None + + def test_node_has_required_attributes(self): + """Test that the node has all required ComfyUI attributes.""" + from kikotools.tools.kiko_workflow_timer.node import KikoWorkflowTimerNode + + # Check required ComfyUI attributes + assert hasattr(KikoWorkflowTimerNode, "INPUT_TYPES") + assert hasattr(KikoWorkflowTimerNode, "RETURN_TYPES") + assert hasattr(KikoWorkflowTimerNode, "FUNCTION") + assert hasattr(KikoWorkflowTimerNode, "CATEGORY") + + def test_node_input_types(self): + """Test that INPUT_TYPES is correctly defined.""" + from kikotools.tools.kiko_workflow_timer.node import KikoWorkflowTimerNode + + input_types = KikoWorkflowTimerNode.INPUT_TYPES() + + # Should have required dict (empty) + assert "required" in input_types + assert input_types["required"] == {} + + # Should have hidden inputs for prompt and unique_id + assert "hidden" in input_types + assert "prompt" in input_types["hidden"] + assert "unique_id" in input_types["hidden"] + assert input_types["hidden"]["prompt"] == "PROMPT" + assert input_types["hidden"]["unique_id"] == "UNIQUE_ID" + + def test_node_return_types(self): + """Test that the node has empty return types (display-only).""" + from kikotools.tools.kiko_workflow_timer.node import KikoWorkflowTimerNode + + assert KikoWorkflowTimerNode.RETURN_TYPES == () + + def test_node_is_output_node(self): + """Test that OUTPUT_NODE is set to True.""" + from kikotools.tools.kiko_workflow_timer.node import KikoWorkflowTimerNode + + assert KikoWorkflowTimerNode.OUTPUT_NODE is True + + def test_node_category(self): + """Test that the node has correct category.""" + from kikotools.tools.kiko_workflow_timer.node import KikoWorkflowTimerNode + + assert "ComfyAssets" in KikoWorkflowTimerNode.CATEGORY + + def test_node_display_name(self): + """Test that the node has a display name.""" + from kikotools.tools.kiko_workflow_timer.node import KikoWorkflowTimerNode + + assert hasattr(KikoWorkflowTimerNode, "DISPLAY_NAME") + assert KikoWorkflowTimerNode.DISPLAY_NAME == "Workflow Timer" + + def test_node_execute_returns_empty(self): + """Test that execute() returns empty dict.""" + from kikotools.tools.kiko_workflow_timer.node import KikoWorkflowTimerNode + + node = KikoWorkflowTimerNode() + result = node.execute() + + assert result == {} + + def test_node_execute_with_kwargs(self): + """Test that execute() handles kwargs correctly.""" + from kikotools.tools.kiko_workflow_timer.node import KikoWorkflowTimerNode + + node = KikoWorkflowTimerNode() + result = node.execute(prompt={}, unique_id="test-123") + + assert result == {} + + def test_node_inherits_from_base(self): + """Test that node inherits from ComfyAssetsBaseNode.""" + from kikotools.tools.kiko_workflow_timer.node import KikoWorkflowTimerNode + from kikotools.base import ComfyAssetsBaseNode + + assert issubclass(KikoWorkflowTimerNode, ComfyAssetsBaseNode) + + +class TestKikoWorkflowTimerModuleInit: + """Test the module's __init__.py exports.""" + + def test_module_exports_node(self): + """Test that the module exports the node class.""" + from kikotools.tools.kiko_workflow_timer import KikoWorkflowTimerNode + + assert KikoWorkflowTimerNode is not None + + def test_module_all_exports(self): + """Test that __all__ is correctly defined.""" + from kikotools.tools import kiko_workflow_timer + + assert hasattr(kiko_workflow_timer, "__all__") + assert "KikoWorkflowTimerNode" in kiko_workflow_timer.__all__ diff --git a/tests/unit/tools/test_sampler_combo.py b/tests/unit/tools/test_sampler_combo.py index 8258beb..08fb12b 100644 --- a/tests/unit/tools/test_sampler_combo.py +++ b/tests/unit/tools/test_sampler_combo.py @@ -178,7 +178,7 @@ class TestSamplerComboNode: def test_return_types_structure(self): """Test that return types are correctly defined.""" - assert SamplerComboNode.RETURN_TYPES == ("SAMPLER", SCHEDULERS, "INT", "FLOAT") + assert SamplerComboNode.RETURN_TYPES == (SAMPLERS, SCHEDULERS, "INT", "FLOAT") assert SamplerComboNode.RETURN_NAMES == ( "sampler_name", "scheduler", diff --git a/tests/unit/tools/test_width_height_to_vec2.py b/tests/unit/tools/test_width_height_to_vec2.py new file mode 100644 index 0000000..9161771 --- /dev/null +++ b/tests/unit/tools/test_width_height_to_vec2.py @@ -0,0 +1,81 @@ +"""Unit tests for Width Height to VEC2 node.""" + +import pytest + +from kikotools.tools.width_height_to_vec2 import WidthHeightToVec2Node + + +class TestWidthHeightToVec2Node: + """Test cases for WidthHeightToVec2Node.""" + + def setup_method(self): + """Set up test fixtures.""" + self.node = WidthHeightToVec2Node() + + def test_basic_int_conversion(self): + """Test basic integer inputs.""" + result = self.node.convert_to_vec2(512, 768) + assert result == ((512, 768),) + + def test_float_conversion(self): + """Test float inputs are converted to int.""" + result = self.node.convert_to_vec2(512.7, 768.3) + assert result == ((512, 768),) + + def test_string_conversion(self): + """Test string inputs are parsed correctly.""" + result = self.node.convert_to_vec2("1024", "768") + assert result == ((1024, 768),) + + def test_string_with_decimal(self): + """Test string with decimal point.""" + result = self.node.convert_to_vec2("1024.5", "768.0") + assert result == ((1024, 768),) + + def test_clamp_max_values(self): + """Test values are clamped to maximum.""" + result = self.node.convert_to_vec2(10000, 9999) + assert result == ((8192, 8192),) + + def test_clamp_min_values(self): + """Test values are clamped to minimum.""" + result = self.node.convert_to_vec2(0, -5) + assert result == ((1, 1),) + + def test_return_type_is_tuple_of_tuple(self): + """Test return type is correct for ComfyUI.""" + result = self.node.convert_to_vec2(512, 512) + assert isinstance(result, tuple) + assert len(result) == 1 + assert isinstance(result[0], tuple) + assert len(result[0]) == 2 + + def test_input_types_defined(self): + """Test INPUT_TYPES is properly defined.""" + input_types = WidthHeightToVec2Node.INPUT_TYPES() + assert "required" in input_types + assert "width" in input_types["required"] + assert "height" in input_types["required"] + + def test_return_types_defined(self): + """Test RETURN_TYPES is properly defined.""" + assert WidthHeightToVec2Node.RETURN_TYPES == ("VEC2",) + assert WidthHeightToVec2Node.RETURN_NAMES == ("vec2",) + + def test_category_set(self): + """Test node category is set.""" + assert "ComfyAssets" in WidthHeightToVec2Node.CATEGORY + + +class TestWidthHeightToVec2Errors: + """Test error handling for WidthHeightToVec2Node.""" + + def setup_method(self): + """Set up test fixtures.""" + self.node = WidthHeightToVec2Node() + + def test_invalid_string_raises_error(self): + """Test invalid string input raises error.""" + with pytest.raises(ValueError) as excinfo: + self.node.convert_to_vec2("not_a_number", 512) + assert "Cannot convert width" in str(excinfo.value) diff --git a/web/display_any.js b/web/display_any.js index cc0ad3e..bcfea50 100644 --- a/web/display_any.js +++ b/web/display_any.js @@ -1,5 +1,5 @@ -import { app } from "../../../scripts/app.js"; -import { ComfyWidgets } from "../../../scripts/widgets.js"; +import { app } from "../../scripts/app.js"; +import { ComfyWidgets } from "../../scripts/widgets.js"; app.registerExtension({ name: "ComfyAssets.DisplayAny", @@ -25,11 +25,11 @@ app.registerExtension({ const lines = text ? text.split('\n') : ['']; const maxLinesPerWidget = 20; const chunks = []; - + for (let i = 0; i < lines.length; i += maxLinesPerWidget) { chunks.push(lines.slice(i, i + maxLinesPerWidget).join('\n')); } - + // Create a widget for each chunk chunks.forEach((chunk, index) => { const w = ComfyWidgets["STRING"](this, `display_${index}`, ["STRING", { multiline: true }], app).widget; @@ -38,7 +38,7 @@ app.registerExtension({ w.inputEl.style.fontFamily = "monospace"; w.value = chunk; }); - + // Add copy button widget const copyWidget = { type: "button", @@ -76,9 +76,9 @@ app.registerExtension({ const onExecuted = nodeType.prototype.onExecuted; nodeType.prototype.onExecuted = function (message) { onExecuted?.apply(this, arguments); - + console.log("DisplayAny onExecuted message:", message); - + if (message?.text && message.text.length > 0) { const displayText = message.text[0]; console.log("DisplayAny displayText:", displayText); @@ -113,14 +113,14 @@ app.registerExtension({ const onNodeCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = function() { onNodeCreated?.apply(this, arguments); - + // Set minimum size - make it wider for better JSON display this.size[0] = Math.max(this.size[0], 450); this.size[1] = Math.max(this.size[1], 250); - + // Add placeholder text populate.call(this, "Value will appear here..."); }; } } -}); \ No newline at end of file +}); diff --git a/web/display_text.js b/web/display_text.js index b0c07f1..a672ef5 100644 --- a/web/display_text.js +++ b/web/display_text.js @@ -1,5 +1,5 @@ -import { app } from "../../../scripts/app.js"; -import { ComfyWidgets } from "../../../scripts/widgets.js"; +import { app } from "../../scripts/app.js"; +import { ComfyWidgets } from "../../scripts/widgets.js"; app.registerExtension({ name: "ComfyAssets.DisplayText", @@ -24,23 +24,23 @@ app.registerExtension({ // Parse the text to detect positive/negative prompt format const posMatch = text.match(/Positive prompt:\s*([\s\S]*?)(?=Negative prompt:|$)/i); const negMatch = text.match(/Negative prompt:\s*([\s\S]*?)(?=\*\*|$)/i); - + if (posMatch && negMatch) { // Extract prompt content let positiveText = posMatch[1].trim(); let negativeText = negMatch[1].trim(); - + // Remove any trailing ** markers const posEndIndex = positiveText.indexOf('**'); if (posEndIndex > 0) { positiveText = positiveText.substring(0, posEndIndex).trim(); } - + const negEndIndex = negativeText.indexOf('**'); if (negEndIndex > 0) { negativeText = negativeText.substring(0, negEndIndex).trim(); } - + // Create header widget for positive prompt const posHeader = ComfyWidgets["STRING"](this, "text_pos_header", ["STRING", { multiline: false }], app).widget; posHeader.inputEl.readOnly = true; @@ -49,13 +49,13 @@ app.registerExtension({ posHeader.inputEl.style.color = "#8f8"; posHeader.inputEl.style.fontWeight = "bold"; posHeader.value = "โ Positive Prompt"; - + // Create positive prompt widget const posWidget = ComfyWidgets["STRING"](this, "text_positive", ["STRING", { multiline: true }], app).widget; posWidget.inputEl.readOnly = true; posWidget.inputEl.style.opacity = 0.9; posWidget.value = positiveText; - + // Create copy button widget for positive prompt const posCopyWidget = { type: "button", @@ -75,7 +75,7 @@ app.registerExtension({ } }; this.addCustomWidget(posCopyWidget); - + // Create header widget for negative prompt const negHeader = ComfyWidgets["STRING"](this, "text_neg_header", ["STRING", { multiline: false }], app).widget; negHeader.inputEl.readOnly = true; @@ -84,13 +84,13 @@ app.registerExtension({ negHeader.inputEl.style.color = "#f88"; negHeader.inputEl.style.fontWeight = "bold"; negHeader.value = "โ Negative Prompt"; - + // Create negative prompt widget const negWidget = ComfyWidgets["STRING"](this, "text_negative", ["STRING", { multiline: true }], app).widget; negWidget.inputEl.readOnly = true; negWidget.inputEl.style.opacity = 0.9; negWidget.value = negativeText; - + // Create copy button widget for negative prompt const negCopyWidget = { type: "button", @@ -110,14 +110,14 @@ app.registerExtension({ } }; this.addCustomWidget(negCopyWidget); - + } else { // Single text display with ComfyUI's standard STRING widget const w = ComfyWidgets["STRING"](this, "text_display", ["STRING", { multiline: true }], app).widget; w.inputEl.readOnly = true; w.inputEl.style.opacity = 0.9; w.value = text; - + // Create copy button widget const copyWidget = { type: "button", @@ -186,14 +186,14 @@ app.registerExtension({ const onNodeCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = function() { onNodeCreated?.apply(this, arguments); - + // Set default size this.size[0] = Math.max(this.size[0], 400); this.size[1] = Math.max(this.size[1], 300); - + // Add placeholder text populate.call(this, "Text will appear here after execution..."); }; } } -}); \ No newline at end of file +}); diff --git a/web/empty_latent_batch_swap.js b/web/empty_latent_batch_swap.js index 636da04..d21f607 100644 --- a/web/empty_latent_batch_swap.js +++ b/web/empty_latent_batch_swap.js @@ -269,7 +269,7 @@ app.registerExtension({ // Add as DOM widget this.swapButtonWidget = this.addDOMWidget( "swap_button", - "div", + "div", buttonContainer ); }; diff --git a/web/gemini_prompt_ui.js b/web/gemini_prompt_ui.js index 9ef8f5e..444dac0 100644 --- a/web/gemini_prompt_ui.js +++ b/web/gemini_prompt_ui.js @@ -1,43 +1,43 @@ -import { app } from "../../../scripts/app.js"; -import { api } from "../../../scripts/api.js"; +import { app } from "../../scripts/app.js"; +import { api } from "../../scripts/api.js"; app.registerExtension({ name: "ComfyAssets.GeminiPrompt", - + async beforeRegisterNodeDef(nodeType, nodeData, app) { if (nodeData.name === "GeminiPrompt") { // Add visual enhancements to the node const onNodeCreated = nodeType.prototype.onNodeCreated; - + nodeType.prototype.onNodeCreated = function() { const result = onNodeCreated?.apply(this, arguments); - + // Store reference to widgets this.promptTypeWidget = this.widgets.find(w => w.name === "prompt_type"); this.modelWidget = this.widgets.find(w => w.name === "model"); this.apiKeyWidget = this.widgets.find(w => w.name === "api_key"); this.customPromptWidget = this.widgets.find(w => w.name === "custom_prompt"); - + // Add helper text button const helpButton = this.addWidget("button", "Help / API Setup", null, () => { this.showHelpDialog(); }); - + // Style the button helpButton.serialize = false; - + // Add refresh models button const refreshButton = this.addWidget("button", "Refresh Model List", null, () => { this.refreshModelList(); }); refreshButton.serialize = false; - + // Add status indicator this.status = this.addWidget("text", "status", "Ready", () => {}, { serialize: false }); this.status.disabled = true; - + // Update custom prompt visibility based on selection if (this.promptTypeWidget && this.customPromptWidget) { const originalCallback = this.promptTypeWidget.callback; @@ -46,19 +46,19 @@ app.registerExtension({ this.updateCustomPromptVisibility(); }; } - + return result; }; - + // Add method to show help dialog nodeType.prototype.showHelpDialog = function() { const helpContent = `
Get your free API key from: Google AI Studio
- +Choose one of these methods:
pip install google-generativeai
-
+
You can override any template by entering your own system prompt in the custom_prompt field.