Files
ComfyAssets-ComfyUI-KikoTools/kikotools/tools/embedding_autocomplete/node.py
T
Vito Sansevero 3e7734bbc3 feat: add KikoEmbeddingAutocomplete with settings registry system
- Implement centralized settings registry for all KikoTools
- Create KikoEmbeddingAutocomplete node with backend API
- Add frontend JavaScript autocomplete widget with ComfyUI integration
- Support for embeddings and LoRAs with smart filtering
- Configurable settings in ComfyUI UI with 🫶 branding
- Include keyboard navigation and real-time suggestions
2025-08-08 13:20:09 -07:00

255 lines
8.5 KiB
Python

"""KikoEmbeddingAutocomplete node for ComfyUI.
Provides autocomplete functionality for embeddings and LoRAs in text inputs.
"""
import os
import json
from typing import Dict, List, Any, Optional
import folder_paths
class KikoEmbeddingAutocomplete:
"""Node that provides embedding autocomplete functionality."""
DISPLAY_NAME = "Embedding Autocomplete"
CATEGORY = "ComfyAssets"
# Settings definition for the settings registry
SETTINGS = {
"enabled": {
"type": "boolean",
"default": True,
"description": "Enable embedding autocomplete",
},
"trigger_chars": {
"type": "combo",
"default": 2,
"options": [1, 2, 3, 4],
"description": "Number of characters before showing suggestions",
},
"max_suggestions": {
"type": "combo",
"default": 20,
"options": [10, 20, 30, 50],
"description": "Maximum number of suggestions to show",
},
"show_embeddings": {
"type": "boolean",
"default": True,
"description": "Show embeddings in suggestions",
},
"show_loras": {
"type": "boolean",
"default": True,
"description": "Show LoRAs in suggestions",
},
"case_sensitive": {
"type": "boolean",
"default": False,
"description": "Case sensitive matching",
},
}
@classmethod
def INPUT_TYPES(cls):
"""Define input types for the node."""
return {
"required": {},
"optional": {
"refresh": ("BOOLEAN", {"default": False}),
},
}
RETURN_TYPES = ("DICT",)
RETURN_NAMES = ("autocomplete_data",)
FUNCTION = "get_autocomplete_data"
def __init__(self):
self.embeddings_cache = None
self.loras_cache = None
def get_autocomplete_data(self, refresh=False):
"""Get autocomplete data for embeddings and LoRAs.
This node doesn't process data in the traditional sense - it provides
autocomplete data to the frontend JavaScript component.
"""
if refresh or self.embeddings_cache is None:
self.refresh_cache()
return (
{
"embeddings": self.embeddings_cache,
"loras": self.loras_cache,
"timestamp": os.path.getmtime(folder_paths.base_path),
},
)
def refresh_cache(self):
"""Refresh the cache of embeddings and LoRAs."""
self.embeddings_cache = self.get_embeddings()
self.loras_cache = self.get_loras()
def get_embeddings(self) -> List[Dict[str, Any]]:
"""Get list of available embeddings."""
embeddings = []
# Get embedding files from ComfyUI's folder system
try:
embedding_files = folder_paths.get_filename_list("embeddings")
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:
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:
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:
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:
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:
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]