feat: add custom words autocomplete support for Prompt node

Adds custom words autocomplete functionality similar to comfyui-custom-scripts,
with the following features:

Backend (Python):
- Create CustomWordsService for CSV parsing and priority-based search
- Add API endpoints: GET/POST /api/lm/custom-words and
  GET /api/lm/custom-words/search
- Share storage with pysssss plugin (checks for their user/autocomplete.txt first)
- Fallback to Lora Manager's user directory for storage

Frontend (JavaScript/Vue):
- Add 'custom_words' and 'prompt' model types to autocomplete system
- Prompt node now supports dual-mode autocomplete:
  * Type 'emb:' prefix → search embeddings
  * Type normally → search custom words (no prefix required)
- Add AUTOCOMPLETE_TEXT_PROMPT widget type
- Update Vue component and composable types

Key Features:
- CSV format: word[,priority] compatible with danbooru-tags.txt
- Priority-based sorting: 20% top priority + prefix + include matches
- Preview tooltip for embeddings (not for custom words)
- Dynamic endpoint switching based on prefix detection

Breaking Changes:
- Prompt (LoraManager) node widget type changed from
  AUTOCOMPLETE_TEXT_EMBEDDINGS to AUTOCOMPLETE_TEXT_PROMPT
- Removed standalone web/comfyui/prompt.js (integrated into main widgets)

Fixes comfy_dir path calculation by prioritizing folder_paths.base_path
from ComfyUI when available, with fallback to computed path.
This commit is contained in:
Will Miao
2026-01-25 12:24:32 +08:00
parent 1f6fc59aa2
commit d5a2bd1e24
13 changed files with 638 additions and 43 deletions

View File

@@ -1201,6 +1201,52 @@ class FileSystemHandler:
return web.json_response({"success": False, "error": str(exc)}, status=500)
class CustomWordsHandler:
"""Handler for custom autocomplete words."""
def __init__(self) -> None:
from ...services.custom_words_service import get_custom_words_service
self._service = get_custom_words_service()
async def get_custom_words(self, request: web.Request) -> web.Response:
"""Get the content of the custom words file."""
try:
content = self._service.get_content()
return web.Response(text=content, content_type="text/plain")
except Exception as exc:
logger.error("Error getting custom words: %s", exc, exc_info=True)
return web.json_response({"error": str(exc)}, status=500)
async def update_custom_words(self, request: web.Request) -> web.Response:
"""Update the custom words file content."""
try:
content = await request.text()
success = self._service.save_words(content)
if success:
return web.Response(status=200)
else:
return web.json_response({"error": "Failed to save custom words"}, status=500)
except Exception as exc:
logger.error("Error updating custom words: %s", exc, exc_info=True)
return web.json_response({"error": str(exc)}, status=500)
async def search_custom_words(self, request: web.Request) -> web.Response:
"""Search custom words with autocomplete."""
try:
search_term = request.query.get("search", "")
limit = int(request.query.get("limit", "20"))
results = self._service.search_words(search_term, limit)
return web.json_response({
"success": True,
"words": results
})
except Exception as exc:
logger.error("Error searching custom words: %s", exc, exc_info=True)
return web.json_response({"error": str(exc)}, status=500)
class NodeRegistryHandler:
def __init__(
self,
@@ -1427,6 +1473,7 @@ class MiscHandlerSet:
model_library: ModelLibraryHandler,
metadata_archive: MetadataArchiveHandler,
filesystem: FileSystemHandler,
custom_words: CustomWordsHandler,
) -> None:
self.health = health
self.settings = settings
@@ -1438,6 +1485,7 @@ class MiscHandlerSet:
self.model_library = model_library
self.metadata_archive = metadata_archive
self.filesystem = filesystem
self.custom_words = custom_words
def to_route_mapping(
self,
@@ -1465,6 +1513,9 @@ class MiscHandlerSet:
"get_model_versions_status": self.model_library.get_model_versions_status,
"open_file_location": self.filesystem.open_file_location,
"open_settings_location": self.filesystem.open_settings_location,
"get_custom_words": self.custom_words.get_custom_words,
"update_custom_words": self.custom_words.update_custom_words,
"search_custom_words": self.custom_words.search_custom_words,
}

View File

@@ -42,6 +42,9 @@ MISC_ROUTE_DEFINITIONS: tuple[RouteDefinition, ...] = (
RouteDefinition("GET", "/api/lm/metadata-archive-status", "get_metadata_archive_status"),
RouteDefinition("GET", "/api/lm/model-versions-status", "get_model_versions_status"),
RouteDefinition("POST", "/api/lm/settings/open-location", "open_settings_location"),
RouteDefinition("GET", "/api/lm/custom-words", "get_custom_words"),
RouteDefinition("POST", "/api/lm/custom-words", "update_custom_words"),
RouteDefinition("GET", "/api/lm/custom-words/search", "search_custom_words"),
)

View File

@@ -18,6 +18,7 @@ from ..services.settings_manager import get_settings_manager
from ..services.downloader import get_downloader
from ..utils.usage_stats import UsageStats
from .handlers.misc_handlers import (
CustomWordsHandler,
FileSystemHandler,
HealthCheckHandler,
LoraCodeHandler,
@@ -117,6 +118,7 @@ class MiscRoutes:
service_registry=self._service_registry_adapter,
metadata_provider_factory=self._metadata_provider_factory,
)
custom_words = CustomWordsHandler()
return self._handler_set_factory(
health=health,
@@ -129,6 +131,7 @@ class MiscRoutes:
model_library=model_library,
metadata_archive=metadata_archive,
filesystem=filesystem,
custom_words=custom_words,
)