Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b881a6304b |
@@ -22,11 +22,11 @@ ComfyUI-KikoTools provides carefully crafted, production-ready nodes grouped und
|
||||
| [⚙️ Sampler Combo](#️-sampler-combo) | Unified sampling configuration interface | 🌀 Samplers |
|
||||
| [📦 Empty Latent Batch](#-empty-latent-batch) | Create empty latent batches with preset support | 📦 Latents |
|
||||
| [💾 Kiko Save Image](#-kiko-save-image) | Enhanced image saving with popup viewer | 💾 Images |
|
||||
| [🎞️ Kiko Film Grain](#️-kiko-film-grain) | Realistic film grain effect with customizable parameters | 🖼️ Images |
|
||||
| [📋 Display Text](#-display-text) | Smart text display with prompt detection | 👁️ Display |
|
||||
| [🤖 Gemini Prompt Engineer](#-gemini-prompt-engineer) | AI-powered image analysis and prompt generation | 🧠 Prompts |
|
||||
| [🔍 Display Any](#-display-any) | Universal debugging tool for any data type | 👁️ Display |
|
||||
| [🖼️ Image to Multiple Of](#️-image-to-multiple-of) | Adjust dimensions to multiples for compatibility | 🖼️ Resolution |
|
||||
| [🔤 Embedding Autocomplete](#-embedding-autocomplete) | Smart autocomplete for embeddings, LoRAs, and tags | ✍️ Text |
|
||||
|
||||
### 🧰 xyz-helpers Tools
|
||||
|
||||
@@ -139,6 +139,27 @@ Enhanced image saving with format selection, quality control, and floating popup
|
||||
|
||||

|
||||
|
||||
#### 🎞️ Kiko Film Grain
|
||||
Apply realistic film grain effects to images, simulating analog film photography aesthetics.
|
||||
|
||||
- **Pure PyTorch Implementation**: GPU-optimized operations without OpenCV dependencies
|
||||
- **Customizable Grain Size**: Scale factor from 0.25 to 2.0 for fine to coarse grain
|
||||
- **Adjustable Intensity**: Strength control from subtle to pronounced grain (0.0-10.0)
|
||||
- **Color Saturation Control**: From monochrome to oversaturated grain (0.0-2.0)
|
||||
- **Film-Like Toe Curve**: Shadow lifting for authentic film look (-0.2 to 0.5)
|
||||
- **Channel-Specific Weighting**: Realistic grain distribution (3x blue, 2x red channel)
|
||||
- **Screen Blend Mode**: Preserves highlights better than standard multiply blending
|
||||
- **Alpha Channel Preservation**: Maintains transparency in RGBA images
|
||||
- **Batch Processing Support**: Efficient processing of multiple images
|
||||
- **Deterministic Seeds**: Reproducible grain patterns for consistent results
|
||||
|
||||
**Use Cases:**
|
||||
- Add vintage film aesthetic to digital images
|
||||
- Simulate high ISO film stock
|
||||
- Create authentic analog photography looks
|
||||
- Add texture to overly clean digital renders
|
||||
- Match grain between composited elements
|
||||
|
||||
#### 📋 Display Text
|
||||
Advanced text display node with intelligent formatting and enhanced user interaction.
|
||||
|
||||
@@ -312,45 +333,6 @@ Unified interface for text encoding and sampler parameter management.
|
||||
- Quick template-based generation
|
||||
- Batch prompt processing
|
||||
|
||||
### 🔤 Embedding Autocomplete
|
||||
|
||||
**Intelligent autocomplete for embeddings, LoRAs, and custom tags in text prompts.**
|
||||
|
||||
<div align="center">
|
||||
<img src="ac-emb.png" width="30%" alt="Embedding Autocomplete" />
|
||||
<img src="ac-lora.png" width="30%" alt="LoRA Autocomplete" />
|
||||
<img src="ac-tag.png" width="30%" alt="Tag Autocomplete" />
|
||||
</div>
|
||||
|
||||
This feature is an enhanced fork of the autocomplete functionality from [ComfyUI-Custom-Scripts](https://github.com/pythongosssss/ComfyUI-Custom-Scripts) by pythongosssss. We've modernized the codebase, fixed existing bugs, and added robust security features.
|
||||
|
||||
**Key Features:**
|
||||
- **Smart Triggers**: Type `embedding:` for embeddings, `<lora:` for LoRAs, or just start typing for tags
|
||||
- **Custom Word Lists**: Load tag databases (like Danbooru tags) from any URL
|
||||
- **Security First**: Comprehensive input validation prevents code injection and XSS attacks
|
||||
- **Flexible Settings**: Customize triggers, auto-insert commas, replace underscores, and more
|
||||
- **Performance Optimized**: Handles 100,000+ tags smoothly with frequency-based sorting
|
||||
- **Visual Polish**: Clean UI with proper scrolling, keyboard navigation, and type indicators
|
||||
|
||||
**Settings Include:**
|
||||
- Enable/disable autocomplete for embeddings, LoRAs, and custom tags
|
||||
- Configurable trigger phrases (e.g., `emb:`, `lora:`, custom shortcuts)
|
||||
- Auto-insert comma after completion
|
||||
- Replace underscores with spaces in tags
|
||||
- Choose insertion keys (Tab, Enter, or both)
|
||||
- Load custom word lists from URLs with security validation
|
||||
|
||||
**Security Features:**
|
||||
- Validates all loaded content to prevent script injection
|
||||
- Blocks dangerous patterns (eval, innerHTML, script tags, etc.)
|
||||
- Safe character whitelist for tags
|
||||
- File size limits to prevent memory exhaustion
|
||||
- Clear error messages for rejected content
|
||||
|
||||
**Credits:**
|
||||
- Original autocomplete concept by [pythongosssss](https://github.com/pythongosssss/ComfyUI-Custom-Scripts)
|
||||
- Enhanced and modernized by KikoTools team
|
||||
|
||||
### 💾 Kiko Save Image Features
|
||||
|
||||
**Use Cases:**
|
||||
|
||||
+1
-85
@@ -13,91 +13,7 @@ except ImportError:
|
||||
from kikotools import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
|
||||
|
||||
# Tell ComfyUI where to find our JavaScript extensions
|
||||
import os
|
||||
|
||||
WEB_DIRECTORY = os.path.join(os.path.dirname(os.path.abspath(__file__)), "web")
|
||||
|
||||
# Import server components at module level to ensure they're available
|
||||
try:
|
||||
from aiohttp import web
|
||||
from server import PromptServer
|
||||
import folder_paths
|
||||
|
||||
print("[KikoTools] Server imports successful")
|
||||
|
||||
# Register autocomplete endpoints directly
|
||||
@PromptServer.instance.routes.get("/kikotools/autocomplete/embeddings")
|
||||
async def get_embeddings(request):
|
||||
"""API endpoint for getting list of embeddings with full paths."""
|
||||
print("[KikoTools] Embeddings endpoint called")
|
||||
try:
|
||||
embedding_files = folder_paths.get_filename_list("embeddings")
|
||||
print(f"[KikoTools] Found {len(embedding_files)} embedding files")
|
||||
# Return embeddings with their subdirectory paths, without extensions
|
||||
embeddings = []
|
||||
for f in embedding_files:
|
||||
# Remove extension but keep subdirectory path
|
||||
clean_path = os.path.splitext(f)[0]
|
||||
embeddings.append(
|
||||
{
|
||||
"file_name": clean_path,
|
||||
"model_name": clean_path,
|
||||
"name": os.path.basename(clean_path),
|
||||
"path": clean_path,
|
||||
}
|
||||
)
|
||||
if len(embeddings) > 0:
|
||||
print(f"[KikoTools] Sample embedding: {embeddings[0]}")
|
||||
print(f"[KikoTools] Returning {len(embeddings)} embeddings with paths")
|
||||
return web.json_response(embeddings)
|
||||
except Exception as e:
|
||||
print(f"[KikoTools] Error getting embeddings: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
return web.json_response([])
|
||||
|
||||
@PromptServer.instance.routes.get("/kikotools/autocomplete/loras")
|
||||
async def get_loras(request):
|
||||
"""API endpoint for getting list of LoRAs."""
|
||||
print("[KikoTools] LoRA endpoint called")
|
||||
try:
|
||||
lora_files = folder_paths.get_filename_list("loras")
|
||||
print(f"[KikoTools] Found {len(lora_files)} LoRA files")
|
||||
# Return LoRAs with paths
|
||||
loras = []
|
||||
for f in lora_files:
|
||||
clean_path = os.path.splitext(f)[0]
|
||||
loras.append(
|
||||
{
|
||||
"name": os.path.basename(clean_path),
|
||||
"path": clean_path,
|
||||
"file": f,
|
||||
}
|
||||
)
|
||||
print(f"[KikoTools] Returning {len(loras)} LoRAs")
|
||||
return web.json_response(loras)
|
||||
except Exception as e:
|
||||
print(f"[KikoTools] Error getting LoRAs: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
return web.json_response([])
|
||||
|
||||
print("[KikoTools] Autocomplete API endpoints registered successfully")
|
||||
print(
|
||||
"[KikoTools] Routes available: /kikotools/autocomplete/embeddings and /kikotools/autocomplete/loras"
|
||||
)
|
||||
|
||||
except ImportError as e:
|
||||
print(f"[KikoTools] Could not import server components: {e}")
|
||||
except Exception as e:
|
||||
print(f"[KikoTools] Unexpected error setting up API: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
|
||||
# API endpoints are registered above at module import time
|
||||
WEB_DIRECTORY = "./web"
|
||||
|
||||
|
||||
def get_version():
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 40 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 41 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 34 KiB |
+12
-15
@@ -3,26 +3,25 @@ KikoTools package initialization and node registry
|
||||
Handles automatic discovery and registration of all ComfyAssets tools
|
||||
"""
|
||||
|
||||
from .tools.resolution_calculator import ResolutionCalculatorNode
|
||||
from .tools.width_height_selector import WidthHeightSelectorNode
|
||||
from .tools.seed_history import SeedHistoryNode
|
||||
from .tools.sampler_combo import SamplerComboNode, SamplerComboCompactNode
|
||||
from .tools.empty_latent_batch import EmptyLatentBatchNode
|
||||
from .tools.kiko_save_image import KikoSaveImageNode
|
||||
from .tools.image_to_multiple_of import ImageToMultipleOfNode
|
||||
from .tools.image_scale_down_by import ImageScaleDownByNode
|
||||
from .tools.gemini_prompt import GeminiPromptNode
|
||||
from .tools.display_any import DisplayAnyNode
|
||||
from .tools.display_text import DisplayTextNode
|
||||
from .tools.embedding_autocomplete import KikoEmbeddingAutocomplete
|
||||
from .tools.empty_latent_batch import EmptyLatentBatchNode
|
||||
from .tools.gemini_prompt import GeminiPromptNode
|
||||
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_save_image import KikoSaveImageNode
|
||||
from .tools.resolution_calculator import ResolutionCalculatorNode
|
||||
from .tools.sampler_combo import SamplerComboCompactNode, SamplerComboNode
|
||||
from .tools.seed_history import SeedHistoryNode
|
||||
from .tools.width_height_selector import WidthHeightSelectorNode
|
||||
from .tools.xyz_helpers import (
|
||||
FluxSamplerParamsNode,
|
||||
LoRAFolderBatchNode,
|
||||
PlotParametersNode,
|
||||
SamplerSelectHelperNode,
|
||||
SchedulerSelectHelperNode,
|
||||
TextEncodeSamplerParamsNode,
|
||||
FluxSamplerParamsNode,
|
||||
PlotParametersNode,
|
||||
LoRAFolderBatchNode,
|
||||
)
|
||||
|
||||
# ComfyUI node registration mappings
|
||||
@@ -46,7 +45,6 @@ NODE_CLASS_MAPPINGS = {
|
||||
"FluxSamplerParams": FluxSamplerParamsNode,
|
||||
"PlotParameters+": PlotParametersNode,
|
||||
"LoRAFolderBatch": LoRAFolderBatchNode,
|
||||
"KikoEmbeddingAutocomplete": KikoEmbeddingAutocomplete,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
@@ -69,7 +67,6 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"FluxSamplerParams": "Flux Sampler Parameters",
|
||||
"PlotParameters+": "Plot Parameters",
|
||||
"LoRAFolderBatch": "LoRA Folder Batch",
|
||||
"KikoEmbeddingAutocomplete": "🫶 Embedding Autocomplete Configuration",
|
||||
}
|
||||
|
||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
"""Tool registry for KikoTools.
|
||||
|
||||
This module provides the central registration system for all KikoTools nodes.
|
||||
"""
|
||||
|
||||
import importlib
|
||||
import os
|
||||
from typing import Dict, List, Any, Optional
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class ToolRegistry:
|
||||
"""Central registry for all KikoTools."""
|
||||
|
||||
def __init__(self):
|
||||
self.tools: Dict[str, Any] = {}
|
||||
self.node_classes: Dict[str, Any] = {}
|
||||
|
||||
def register_tool(self, tool_name: str, node_class: Any) -> None:
|
||||
"""Register a tool and its node class.
|
||||
|
||||
Args:
|
||||
tool_name: Name of the tool
|
||||
node_class: The ComfyUI node class
|
||||
"""
|
||||
self.tools[tool_name] = node_class
|
||||
|
||||
# Also register by class name for ComfyUI
|
||||
class_name = node_class.__name__
|
||||
self.node_classes[class_name] = node_class
|
||||
|
||||
def discover_tools(self) -> None:
|
||||
"""Automatically discover and load all tools in the tools directory."""
|
||||
tools_dir = Path(__file__).parent.parent / "tools"
|
||||
|
||||
if not tools_dir.exists():
|
||||
return
|
||||
|
||||
for tool_dir in tools_dir.iterdir():
|
||||
if tool_dir.is_dir() and not tool_dir.name.startswith("_"):
|
||||
self._load_tool(tool_dir.name)
|
||||
|
||||
def _load_tool(self, tool_name: str) -> None:
|
||||
"""Load a single tool module.
|
||||
|
||||
Args:
|
||||
tool_name: Name of the tool directory
|
||||
"""
|
||||
try:
|
||||
# Try to import the tool's node module
|
||||
module = importlib.import_module(f"kikotools.tools.{tool_name}.node")
|
||||
|
||||
# Look for node classes (classes with ComfyUI node attributes)
|
||||
for attr_name in dir(module):
|
||||
attr = getattr(module, attr_name)
|
||||
if (
|
||||
isinstance(attr, type)
|
||||
and hasattr(attr, "INPUT_TYPES")
|
||||
and hasattr(attr, "FUNCTION")
|
||||
):
|
||||
self.register_tool(tool_name, attr)
|
||||
|
||||
# If the tool has settings, register them
|
||||
if hasattr(attr, "SETTINGS"):
|
||||
from .settings import settings_registry
|
||||
|
||||
settings_registry.register_tool_settings(
|
||||
tool_name,
|
||||
getattr(
|
||||
attr,
|
||||
"DISPLAY_NAME",
|
||||
tool_name.replace("_", " ").title(),
|
||||
),
|
||||
attr.SETTINGS,
|
||||
)
|
||||
|
||||
except ImportError as e:
|
||||
# Tool might not have a node.py file yet
|
||||
pass
|
||||
|
||||
def get_node_class_mappings(self) -> Dict[str, Any]:
|
||||
"""Get node class mappings for ComfyUI registration."""
|
||||
return self.node_classes.copy()
|
||||
|
||||
def get_node_display_name_mappings(self) -> Dict[str, str]:
|
||||
"""Get display name mappings for ComfyUI."""
|
||||
mappings = {}
|
||||
for class_name, node_class in self.node_classes.items():
|
||||
if hasattr(node_class, "DISPLAY_NAME"):
|
||||
mappings[class_name] = node_class.DISPLAY_NAME
|
||||
else:
|
||||
# Generate a display name from class name
|
||||
mappings[class_name] = class_name.replace("Kiko", "").replace(
|
||||
"Node", ""
|
||||
)
|
||||
return mappings
|
||||
@@ -1,201 +0,0 @@
|
||||
"""Settings registry for KikoTools.
|
||||
|
||||
This module provides a centralized settings management system for all KikoTools.
|
||||
Tools can register their settings, which are then exposed in ComfyUI's settings UI.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import Dict, Any, List, Optional, Union
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
|
||||
@dataclass
|
||||
class SettingDefinition:
|
||||
"""Definition of a single setting."""
|
||||
|
||||
id: str
|
||||
name: str
|
||||
type: str # "boolean", "combo", "number", "string", "custom"
|
||||
default: Any
|
||||
description: Optional[str] = None
|
||||
options: Optional[Union[List[Any], Dict[str, Any]]] = None
|
||||
min_value: Optional[float] = None
|
||||
max_value: Optional[float] = None
|
||||
step: Optional[float] = None
|
||||
on_change: Optional[str] = None # JavaScript callback as string
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToolSettings:
|
||||
"""Settings collection for a single tool."""
|
||||
|
||||
tool_name: str
|
||||
display_name: str
|
||||
settings: List[SettingDefinition] = field(default_factory=list)
|
||||
|
||||
|
||||
class SettingsRegistry:
|
||||
"""Central registry for all KikoTools settings."""
|
||||
|
||||
def __init__(self):
|
||||
self.tools: Dict[str, ToolSettings] = {}
|
||||
self.settings_by_id: Dict[str, SettingDefinition] = {}
|
||||
|
||||
def register_tool_settings(
|
||||
self, tool_name: str, display_name: str, settings: Dict[str, Dict[str, Any]]
|
||||
) -> None:
|
||||
"""Register settings for a tool.
|
||||
|
||||
Args:
|
||||
tool_name: Internal tool identifier (e.g., "embedding_autocomplete")
|
||||
display_name: Display name for the tool (e.g., "Embedding Autocomplete")
|
||||
settings: Dictionary of setting configurations
|
||||
{
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"default": True,
|
||||
"description": "Enable embedding autocomplete"
|
||||
},
|
||||
"max_suggestions": {
|
||||
"type": "combo",
|
||||
"default": 20,
|
||||
"options": [10, 20, 50],
|
||||
"description": "Maximum number of suggestions"
|
||||
}
|
||||
}
|
||||
"""
|
||||
tool_settings = ToolSettings(tool_name, display_name)
|
||||
|
||||
for setting_key, config in settings.items():
|
||||
# Generate fully qualified setting ID
|
||||
setting_id = f"kikotools.{tool_name}.{setting_key}"
|
||||
|
||||
# Create display name with branding
|
||||
setting_name = f"🫶 {display_name}: {setting_key.replace('_', ' ').title()}"
|
||||
|
||||
setting_def = SettingDefinition(
|
||||
id=setting_id,
|
||||
name=setting_name,
|
||||
type=config.get("type", "string"),
|
||||
default=config.get("default"),
|
||||
description=config.get("description"),
|
||||
options=config.get("options"),
|
||||
min_value=config.get("min"),
|
||||
max_value=config.get("max"),
|
||||
step=config.get("step"),
|
||||
on_change=config.get("on_change"),
|
||||
)
|
||||
|
||||
tool_settings.settings.append(setting_def)
|
||||
self.settings_by_id[setting_id] = setting_def
|
||||
|
||||
self.tools[tool_name] = tool_settings
|
||||
|
||||
def get_setting(self, setting_id: str) -> Optional[SettingDefinition]:
|
||||
"""Get a setting definition by ID."""
|
||||
return self.settings_by_id.get(setting_id)
|
||||
|
||||
def get_tool_settings(self, tool_name: str) -> Optional[ToolSettings]:
|
||||
"""Get all settings for a tool."""
|
||||
return self.tools.get(tool_name)
|
||||
|
||||
def generate_frontend_registration(self) -> str:
|
||||
"""Generate JavaScript code for frontend settings registration."""
|
||||
js_lines = [
|
||||
"// Auto-generated KikoTools settings registration",
|
||||
"// This file is automatically generated by the settings registry",
|
||||
"",
|
||||
"import { app } from '../../scripts/app.js';",
|
||||
"",
|
||||
"app.registerExtension({",
|
||||
" name: 'kikotools.settings',",
|
||||
" async init() {",
|
||||
" // Register all KikoTools settings",
|
||||
]
|
||||
|
||||
for tool_name, tool_settings in self.tools.items():
|
||||
js_lines.append(f" // {tool_settings.display_name} settings")
|
||||
|
||||
for setting in tool_settings.settings:
|
||||
js_lines.append(f" app.ui.settings.addSetting({{")
|
||||
js_lines.append(f' id: "{setting.id}",')
|
||||
js_lines.append(f' name: "{setting.name}",')
|
||||
js_lines.append(
|
||||
f" defaultValue: {self._js_value(setting.default)},"
|
||||
)
|
||||
js_lines.append(f' type: "{setting.type}",')
|
||||
|
||||
if setting.description:
|
||||
js_lines.append(f' tooltip: "{setting.description}",')
|
||||
|
||||
if setting.type == "combo" and setting.options:
|
||||
js_lines.append(f" options: (value) => {{")
|
||||
js_lines.append(
|
||||
f" const options = {json.dumps(setting.options)};"
|
||||
)
|
||||
js_lines.append(f" return options.map(opt => ({{")
|
||||
js_lines.append(f" value: opt,")
|
||||
js_lines.append(f" text: String(opt),")
|
||||
js_lines.append(f" selected: opt === value")
|
||||
js_lines.append(f" }}));")
|
||||
js_lines.append(f" }},")
|
||||
|
||||
if setting.type == "number":
|
||||
if setting.min_value is not None:
|
||||
js_lines.append(f" min: {setting.min_value},")
|
||||
if setting.max_value is not None:
|
||||
js_lines.append(f" max: {setting.max_value},")
|
||||
if setting.step is not None:
|
||||
js_lines.append(f" step: {setting.step},")
|
||||
|
||||
if setting.on_change:
|
||||
js_lines.append(f" onChange(value) {{")
|
||||
js_lines.append(f" {setting.on_change}")
|
||||
js_lines.append(f" }}")
|
||||
|
||||
js_lines.append(f" }});")
|
||||
js_lines.append("")
|
||||
|
||||
js_lines.extend([" }", "});", ""])
|
||||
|
||||
return "\n".join(js_lines)
|
||||
|
||||
def _js_value(self, value: Any) -> str:
|
||||
"""Convert Python value to JavaScript literal."""
|
||||
if isinstance(value, bool):
|
||||
return "true" if value else "false"
|
||||
elif isinstance(value, str):
|
||||
return f'"{value}"'
|
||||
elif value is None:
|
||||
return "null"
|
||||
else:
|
||||
return str(value)
|
||||
|
||||
def save_frontend_settings(
|
||||
self, output_path: str = "web/js/kikoSettings.js"
|
||||
) -> None:
|
||||
"""Save the generated frontend settings to a file."""
|
||||
js_content = self.generate_frontend_registration()
|
||||
|
||||
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
||||
with open(output_path, "w") as f:
|
||||
f.write(js_content)
|
||||
|
||||
def get_all_settings(self) -> Dict[str, Any]:
|
||||
"""Get all registered settings as a dictionary."""
|
||||
result = {}
|
||||
for tool_name, tool_settings in self.tools.items():
|
||||
result[tool_name] = {
|
||||
"display_name": tool_settings.display_name,
|
||||
"settings": {
|
||||
setting.id.split(".")[-1]: {
|
||||
"type": setting.type,
|
||||
"default": setting.default,
|
||||
"description": setting.description,
|
||||
"options": setting.options,
|
||||
}
|
||||
for setting in tool_settings.settings
|
||||
},
|
||||
}
|
||||
return result
|
||||
@@ -1,5 +0,0 @@
|
||||
"""Embedding Autocomplete tool for KikoTools."""
|
||||
|
||||
from .node import KikoEmbeddingAutocomplete
|
||||
|
||||
__all__ = ["KikoEmbeddingAutocomplete"]
|
||||
@@ -1,291 +0,0 @@
|
||||
"""KikoEmbeddingAutocomplete node for ComfyUI.
|
||||
|
||||
Provides autocomplete functionality for embeddings and LoRAs in text inputs.
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Dict, List, Any
|
||||
|
||||
try:
|
||||
import folder_paths
|
||||
except ImportError:
|
||||
# For testing outside ComfyUI environment
|
||||
folder_paths = None
|
||||
|
||||
|
||||
class KikoEmbeddingAutocomplete:
|
||||
"""Node that provides embedding autocomplete functionality."""
|
||||
|
||||
DISPLAY_NAME = "🫶 Embedding Autocomplete Settings"
|
||||
CATEGORY = "ComfyAssets"
|
||||
|
||||
# Settings definition for the settings registry
|
||||
SETTINGS = {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"default": True,
|
||||
"description": "Enable autocomplete",
|
||||
},
|
||||
"show_embeddings": {
|
||||
"type": "boolean",
|
||||
"default": True,
|
||||
"description": "Show embeddings in autocomplete",
|
||||
},
|
||||
"show_loras": {
|
||||
"type": "boolean",
|
||||
"default": True,
|
||||
"description": "Show LoRAs in autocomplete",
|
||||
},
|
||||
"embedding_trigger": {
|
||||
"type": "text",
|
||||
"default": "embedding:",
|
||||
"description": "Trigger text for embeddings (e.g., 'embedding:', 'emb:', or custom)",
|
||||
},
|
||||
"lora_trigger": {
|
||||
"type": "text",
|
||||
"default": "<lora:",
|
||||
"description": "Trigger text for LoRAs (e.g., '<lora:', 'lora:', or custom)",
|
||||
},
|
||||
"quick_trigger": {
|
||||
"type": "text",
|
||||
"default": "em",
|
||||
"description": "Quick trigger to show embeddings (e.g., 'em', 'emb', or disabled with '')",
|
||||
},
|
||||
"min_chars": {
|
||||
"type": "combo",
|
||||
"default": 2,
|
||||
"options": [1, 2, 3, 4, 5],
|
||||
"description": "Minimum characters before showing suggestions",
|
||||
},
|
||||
"max_suggestions": {
|
||||
"type": "combo",
|
||||
"default": 20,
|
||||
"options": [5, 10, 15, 20, 30, 50, 100],
|
||||
"description": "Maximum number of suggestions to display",
|
||||
},
|
||||
"sort_by_directory": {
|
||||
"type": "boolean",
|
||||
"default": True,
|
||||
"description": "Group suggestions by directory",
|
||||
},
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
"""Define input types for the node."""
|
||||
return {
|
||||
"required": {},
|
||||
"hidden": {
|
||||
"unique_id": "UNIQUE_ID",
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ()
|
||||
RETURN_NAMES = ()
|
||||
FUNCTION = "update_settings"
|
||||
OUTPUT_NODE = True
|
||||
|
||||
@classmethod
|
||||
def VALIDATE_INPUTS(cls, **kwargs):
|
||||
return True
|
||||
|
||||
def __init__(self):
|
||||
self.embeddings_cache = None
|
||||
self.loras_cache = None
|
||||
|
||||
def update_settings(self, unique_id=None):
|
||||
"""Update settings display.
|
||||
|
||||
This node serves as a settings indicator.
|
||||
Actual settings are configured in ComfyUI Settings menu.
|
||||
"""
|
||||
# This node doesn't actually process anything
|
||||
# It's just a visual indicator that autocomplete is available
|
||||
return ()
|
||||
|
||||
def refresh_cache(self):
|
||||
"""Refresh the cache of embeddings and LoRAs."""
|
||||
print("[KikoEmbeddingAutocomplete] Refreshing cache...")
|
||||
self.embeddings_cache = self.get_embeddings()
|
||||
self.loras_cache = self.get_loras()
|
||||
print(
|
||||
f"[KikoEmbeddingAutocomplete] Cached {len(self.embeddings_cache)} embeddings, {len(self.loras_cache)} LoRAs"
|
||||
)
|
||||
|
||||
def get_embeddings(self) -> List[Dict[str, Any]]:
|
||||
"""Get list of available embeddings."""
|
||||
embeddings = []
|
||||
|
||||
# Get embedding files from ComfyUI's folder system
|
||||
try:
|
||||
print("[KikoEmbeddingAutocomplete] Getting embeddings list...")
|
||||
if folder_paths is None:
|
||||
return embeddings
|
||||
embedding_files = folder_paths.get_filename_list("embeddings")
|
||||
print(
|
||||
f"[KikoEmbeddingAutocomplete] Found {len(embedding_files)} embedding files"
|
||||
)
|
||||
for file in embedding_files:
|
||||
name = os.path.splitext(file)[0]
|
||||
embeddings.append(
|
||||
{
|
||||
"name": name,
|
||||
"file": file,
|
||||
"type": "embedding",
|
||||
"display": f"embedding:{name}",
|
||||
"value": f"embedding:{name}",
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Error loading embeddings: {e}")
|
||||
|
||||
return embeddings
|
||||
|
||||
def get_loras(self) -> List[Dict[str, Any]]:
|
||||
"""Get list of available LoRAs."""
|
||||
loras = []
|
||||
|
||||
# Get LoRA files from ComfyUI's folder system
|
||||
try:
|
||||
if folder_paths is None:
|
||||
return loras
|
||||
lora_files = folder_paths.get_filename_list("loras")
|
||||
for file in lora_files:
|
||||
name = os.path.splitext(file)[0]
|
||||
loras.append(
|
||||
{
|
||||
"name": name,
|
||||
"file": file,
|
||||
"type": "lora",
|
||||
"display": f"<lora:{name}:1.0>",
|
||||
"value": f"<lora:{name}:1.0>",
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Error loading LoRAs: {e}")
|
||||
|
||||
return loras
|
||||
|
||||
@classmethod
|
||||
def IS_CHANGED(cls, **kwargs):
|
||||
"""Check if the node needs to be re-executed."""
|
||||
# Always re-execute if refresh is True
|
||||
if kwargs.get("refresh", False):
|
||||
return float("NaN")
|
||||
|
||||
# Check if embeddings/loras folders have changed
|
||||
try:
|
||||
if folder_paths is None:
|
||||
return 0
|
||||
embeddings_path = folder_paths.get_folder_paths("embeddings")[0]
|
||||
loras_path = folder_paths.get_folder_paths("loras")[0]
|
||||
|
||||
# Return combined modification time
|
||||
return os.path.getmtime(embeddings_path) + os.path.getmtime(loras_path)
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
|
||||
class KikoEmbeddingAutocompleteAPI:
|
||||
"""API endpoints for embedding autocomplete."""
|
||||
|
||||
@staticmethod
|
||||
def get_suggestions(
|
||||
prefix: str,
|
||||
max_results: int = 20,
|
||||
include_embeddings: bool = True,
|
||||
include_loras: bool = True,
|
||||
case_sensitive: bool = False,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Get autocomplete suggestions for a given prefix.
|
||||
|
||||
Args:
|
||||
prefix: The text prefix to match
|
||||
max_results: Maximum number of results to return
|
||||
include_embeddings: Include embeddings in results
|
||||
include_loras: Include LoRAs in results
|
||||
case_sensitive: Use case-sensitive matching
|
||||
|
||||
Returns:
|
||||
List of suggestion dictionaries
|
||||
"""
|
||||
suggestions = []
|
||||
|
||||
# Normalize prefix for matching
|
||||
match_prefix = prefix if case_sensitive else prefix.lower()
|
||||
|
||||
# Get embeddings
|
||||
if include_embeddings:
|
||||
try:
|
||||
if folder_paths is None:
|
||||
embedding_files = []
|
||||
else:
|
||||
embedding_files = folder_paths.get_filename_list("embeddings")
|
||||
for file in embedding_files:
|
||||
name = os.path.splitext(file)[0]
|
||||
match_name = name if case_sensitive else name.lower()
|
||||
|
||||
# Check for match
|
||||
if match_name.startswith(match_prefix):
|
||||
suggestions.append(
|
||||
{
|
||||
"name": name,
|
||||
"type": "embedding",
|
||||
"display": f"embedding:{name}",
|
||||
"value": f"embedding:{name}",
|
||||
"priority": 1 if match_name == match_prefix else 0,
|
||||
}
|
||||
)
|
||||
elif match_prefix in match_name:
|
||||
suggestions.append(
|
||||
{
|
||||
"name": name,
|
||||
"type": "embedding",
|
||||
"display": f"embedding:{name}",
|
||||
"value": f"embedding:{name}",
|
||||
"priority": -1,
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Error loading embeddings: {e}")
|
||||
|
||||
# Get LoRAs
|
||||
if include_loras:
|
||||
try:
|
||||
if folder_paths is None:
|
||||
lora_files = []
|
||||
else:
|
||||
lora_files = folder_paths.get_filename_list("loras")
|
||||
for file in lora_files:
|
||||
name = os.path.splitext(file)[0]
|
||||
match_name = name if case_sensitive else name.lower()
|
||||
|
||||
# Check for match
|
||||
if match_name.startswith(match_prefix):
|
||||
suggestions.append(
|
||||
{
|
||||
"name": name,
|
||||
"type": "lora",
|
||||
"display": f"<lora:{name}:1.0>",
|
||||
"value": f"<lora:{name}:1.0>",
|
||||
"priority": 1 if match_name == match_prefix else 0,
|
||||
}
|
||||
)
|
||||
elif match_prefix in match_name:
|
||||
suggestions.append(
|
||||
{
|
||||
"name": name,
|
||||
"type": "lora",
|
||||
"display": f"<lora:{name}:1.0>",
|
||||
"value": f"<lora:{name}:1.0>",
|
||||
"priority": -1,
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Error loading LoRAs: {e}")
|
||||
|
||||
# Sort by priority and name
|
||||
suggestions.sort(key=lambda x: (-x["priority"], x["name"]))
|
||||
|
||||
# Limit results
|
||||
return suggestions[:max_results]
|
||||
@@ -3,17 +3,10 @@ pytest configuration and fixtures for ComfyUI-KikoTools testing
|
||||
Provides mock ComfyUI environments and test data
|
||||
"""
|
||||
|
||||
import sys
|
||||
import pytest
|
||||
import torch
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
# Mock folder_paths module before any imports that might use it
|
||||
sys.modules["folder_paths"] = MagicMock()
|
||||
sys.modules["folder_paths"].get_filename_list = MagicMock(return_value=[])
|
||||
sys.modules["folder_paths"].get_folder_paths = MagicMock(return_value=["/mock/path"])
|
||||
sys.modules["folder_paths"].base_path = "/mock/base"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_image_tensor():
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
"""Basic tests for KikoEmbeddingAutocomplete."""
|
||||
|
||||
import sys
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
|
||||
def test_import():
|
||||
"""Test that the module can be imported."""
|
||||
from kikotools.tools.embedding_autocomplete import KikoEmbeddingAutocomplete
|
||||
|
||||
assert KikoEmbeddingAutocomplete is not None
|
||||
assert (
|
||||
KikoEmbeddingAutocomplete.DISPLAY_NAME == "🫶 Embedding Autocomplete Settings"
|
||||
)
|
||||
assert KikoEmbeddingAutocomplete.CATEGORY == "ComfyAssets"
|
||||
|
||||
|
||||
def test_settings_defined():
|
||||
"""Test that settings are properly defined."""
|
||||
from kikotools.tools.embedding_autocomplete import KikoEmbeddingAutocomplete
|
||||
|
||||
settings = KikoEmbeddingAutocomplete.SETTINGS
|
||||
assert "enabled" in settings
|
||||
assert "min_chars" in settings # Changed from trigger_chars
|
||||
assert "max_suggestions" in settings
|
||||
assert "show_embeddings" in settings
|
||||
assert "show_loras" in settings
|
||||
assert "embedding_trigger" in settings
|
||||
assert "lora_trigger" in settings
|
||||
assert "quick_trigger" in settings
|
||||
assert "sort_by_directory" in settings
|
||||
|
||||
# Check settings structure
|
||||
assert settings["enabled"]["type"] == "boolean"
|
||||
assert settings["enabled"]["default"] is True
|
||||
assert settings["min_chars"]["type"] == "combo"
|
||||
assert settings["min_chars"]["options"] == [1, 2, 3, 4, 5]
|
||||
|
||||
|
||||
def test_input_types():
|
||||
"""Test INPUT_TYPES class method."""
|
||||
from kikotools.tools.embedding_autocomplete import KikoEmbeddingAutocomplete
|
||||
|
||||
input_types = KikoEmbeddingAutocomplete.INPUT_TYPES()
|
||||
assert "required" in input_types
|
||||
assert "hidden" in input_types
|
||||
assert input_types["required"] == {} # No required inputs
|
||||
assert "unique_id" in input_types["hidden"]
|
||||
|
||||
|
||||
def test_api_suggestions():
|
||||
"""Test the API suggestions method."""
|
||||
from kikotools.tools.embedding_autocomplete.node import (
|
||||
KikoEmbeddingAutocompleteAPI,
|
||||
folder_paths,
|
||||
)
|
||||
|
||||
# Mock folder_paths if it exists (will be None in tests)
|
||||
with patch("kikotools.tools.embedding_autocomplete.node.folder_paths") as mock_fp:
|
||||
mock_fp.get_filename_list = MagicMock(
|
||||
side_effect=lambda x: (
|
||||
["test1.pt", "test2.safetensors"]
|
||||
if x == "embeddings"
|
||||
else ["lora1.pt", "lora2.safetensors"]
|
||||
)
|
||||
)
|
||||
|
||||
# Test with embeddings
|
||||
suggestions = KikoEmbeddingAutocompleteAPI.get_suggestions(
|
||||
prefix="test", include_embeddings=True, include_loras=False
|
||||
)
|
||||
|
||||
assert len(suggestions) == 2
|
||||
assert suggestions[0]["type"] == "embedding"
|
||||
assert suggestions[0]["name"] == "test1"
|
||||
|
||||
# Test with LoRAs
|
||||
suggestions = KikoEmbeddingAutocompleteAPI.get_suggestions(
|
||||
prefix="lora", include_embeddings=False, include_loras=True
|
||||
)
|
||||
|
||||
assert len(suggestions) == 2
|
||||
assert suggestions[0]["type"] == "lora"
|
||||
assert "<lora:" in suggestions[0]["value"]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_import()
|
||||
test_settings_defined()
|
||||
test_input_types()
|
||||
test_api_suggestions()
|
||||
print("All tests passed!")
|
||||
@@ -1,38 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Test script to check how ComfyUI returns embedding paths."""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add ComfyUI to path if available
|
||||
comfyui_path = os.path.expanduser("~/ComfyUI")
|
||||
if os.path.exists(comfyui_path):
|
||||
sys.path.insert(0, comfyui_path)
|
||||
|
||||
try:
|
||||
import folder_paths
|
||||
|
||||
print("Testing embedding paths...")
|
||||
print("=" * 50)
|
||||
|
||||
# Get embeddings
|
||||
embeddings = folder_paths.get_filename_list("embeddings")
|
||||
print(f"Total embeddings found: {len(embeddings)}")
|
||||
print("\nFirst 20 embeddings:")
|
||||
for i, emb in enumerate(embeddings[:20]):
|
||||
print(f" {i+1}. '{emb}'")
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print("Checking for path separators...")
|
||||
has_paths = any("/" in emb or "\\" in emb for emb in embeddings)
|
||||
print(f"Contains path separators: {has_paths}")
|
||||
|
||||
if has_paths:
|
||||
print("\nEmbeddings with paths:")
|
||||
for emb in embeddings[:10]:
|
||||
if "/" in emb or "\\" in emb:
|
||||
print(f" - {emb}")
|
||||
|
||||
except ImportError as e:
|
||||
print(f"Could not import folder_paths: {e}")
|
||||
print("\nThis script should be run from within ComfyUI environment")
|
||||
@@ -1,60 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Test what folder_paths.get_filename_list actually returns."""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add ComfyUI to path
|
||||
comfyui_path = "/home/vito/ai-apps/ComfyUI-3.12"
|
||||
if os.path.exists(comfyui_path):
|
||||
sys.path.insert(0, comfyui_path)
|
||||
# Set the working directory for folder_paths
|
||||
os.environ["COMFYUI_PATH"] = comfyui_path
|
||||
|
||||
try:
|
||||
import folder_paths
|
||||
|
||||
print("Testing folder_paths.get_filename_list('embeddings')...")
|
||||
print("=" * 60)
|
||||
|
||||
embeddings = folder_paths.get_filename_list("embeddings")
|
||||
print(f"Total embeddings: {len(embeddings)}")
|
||||
|
||||
print("\nFirst 10 embeddings:")
|
||||
for i, emb in enumerate(embeddings[:10]):
|
||||
print(f" {i+1}. '{emb}'")
|
||||
|
||||
# Check if any have paths
|
||||
with_paths = [e for e in embeddings if "/" in e or "\\" in e]
|
||||
print(f"\nEmbeddings with path separators: {len(with_paths)}")
|
||||
if with_paths:
|
||||
print("Examples:")
|
||||
for e in with_paths[:5]:
|
||||
print(f" - '{e}'")
|
||||
|
||||
# Check the actual folder structure
|
||||
print("\n" + "=" * 60)
|
||||
print("Checking actual folder structure...")
|
||||
emb_folders = folder_paths.get_folder_paths("embeddings")
|
||||
print(f"Embedding folders: {emb_folders}")
|
||||
|
||||
if emb_folders:
|
||||
emb_dir = emb_folders[0]
|
||||
print(f"\nContents of {emb_dir}:")
|
||||
for root, dirs, files in os.walk(emb_dir):
|
||||
rel_root = os.path.relpath(root, emb_dir)
|
||||
if rel_root == ".":
|
||||
rel_root = ""
|
||||
for f in files[:5]: # Show first 5 files in each dir
|
||||
if f.endswith((".pt", ".safetensors", ".ckpt")):
|
||||
full_path = os.path.join(rel_root, f) if rel_root else f
|
||||
print(f" - '{full_path}'")
|
||||
if len(files) > 5:
|
||||
print(f" ... and {len(files)-5} more files")
|
||||
if dirs:
|
||||
print(f" Subdirectories: {dirs}")
|
||||
|
||||
except ImportError as e:
|
||||
print(f"Could not import folder_paths: {e}")
|
||||
else:
|
||||
print(f"ComfyUI not found at {comfyui_path}")
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user