Borrowed from VoxCPM (HF download stack, settings, routes support) and VibeVoice (registry, patcher, device/dtype/ attention). Deletes chatterbox_handler.py.
196 lines
6.9 KiB
Python
196 lines
6.9 KiB
Python
"""ComfyUI folder-registration helpers for Chatterbox TTS model assets.
|
|
|
|
``folder_paths`` arrives as an **injected parameter** (``folder_paths_module``)
|
|
rather than being imported here. That is what makes registration unit-testable
|
|
without booting ComfyUI, and it is why this module has no ``comfy`` /
|
|
``folder_paths`` import at all -- not even lazily.
|
|
|
|
Two rules shape everything below:
|
|
|
|
* **Register every TTS root, not the first one.** A machine resolves models
|
|
across several roots at once -- the primary ``<models>/tts`` plus whatever
|
|
``extra_model_paths.yaml`` contributes. A ``chatterbox`` sub-directory has to
|
|
be registered under *all* of them, otherwise a model installed under a
|
|
secondary root is invisible even though the file is right there.
|
|
* **Never clobber another node's registration.** ``tts`` is a shared key: if
|
|
another TTS node already registered it, its path list and its extension set
|
|
are extended, never replaced.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
from typing import Any, Iterable
|
|
|
|
__all__ = [
|
|
"TTS_FOLDER_KEY",
|
|
"CHATTERBOX_FOLDER_KEY",
|
|
"CHATTERBOX_SUBDIR",
|
|
"CHATTERBOX_EXTENSIONS",
|
|
"register_chatterbox_folders",
|
|
"get_registered_tts_roots",
|
|
"ensure_chatterbox_directories",
|
|
"ensure_directory",
|
|
]
|
|
|
|
|
|
# Shared ComfyUI folder key. Not ours alone -- every TTS node may claim it.
|
|
TTS_FOLDER_KEY = "tts"
|
|
|
|
# Chatterbox-specific sub-key, registered *under every* TTS root.
|
|
CHATTERBOX_FOLDER_KEY = "chatterbox"
|
|
CHATTERBOX_SUBDIR = "chatterbox"
|
|
|
|
# Extensions ComfyUI's model browser uses to filter a folder listing.
|
|
CHATTERBOX_EXTENSIONS = frozenset(
|
|
{".safetensors", ".pt", ".bin", ".ckpt", ".json", ".yaml", ".yml"}
|
|
)
|
|
|
|
|
|
def _normalize(path: str) -> str:
|
|
"""Absolute, normalised form of ``path`` -- the identity used for dedup."""
|
|
return os.path.abspath(os.path.normpath(os.fspath(path)))
|
|
|
|
|
|
def _append_unique_path(paths: list[str], candidate: str) -> bool:
|
|
"""Append a normalised path once, preserving registration order.
|
|
|
|
Returns ``True`` when the path was actually appended. ``paths`` is mutated
|
|
in place because it is the list object ComfyUI's ``folder_names_and_paths``
|
|
already holds; rebinding a new list would silently detach it.
|
|
"""
|
|
if not candidate:
|
|
return False
|
|
normalized = _normalize(candidate)
|
|
existing = {_normalize(path) for path in paths}
|
|
if normalized in existing:
|
|
return False
|
|
paths.append(normalized)
|
|
return True
|
|
|
|
|
|
def _ensure_entry(
|
|
folder_names_and_paths: dict, key: str, extensions: Iterable[str]
|
|
) -> list[str]:
|
|
"""Return the path list for ``key``, creating the entry only if absent.
|
|
|
|
An entry another node already created is returned untouched apart from the
|
|
extensions being unioned in: replacing the tuple would drop that node's
|
|
paths.
|
|
"""
|
|
entry = folder_names_and_paths.get(key)
|
|
if entry is None:
|
|
paths: list[str] = []
|
|
folder_names_and_paths[key] = (paths, set(extensions))
|
|
return paths
|
|
|
|
if not isinstance(entry, (tuple, list)) or len(entry) < 1:
|
|
# Defensive: a foreign entry of an unexpected shape is replaced rather
|
|
# than trusted, because a non-list first element cannot be appended to.
|
|
paths = []
|
|
folder_names_and_paths[key] = (paths, set(extensions))
|
|
return paths
|
|
|
|
paths = entry[0]
|
|
if not isinstance(paths, list):
|
|
paths = list(paths)
|
|
folder_names_and_paths[key] = (paths, entry[1])
|
|
|
|
if len(entry) > 1 and isinstance(entry[1], set):
|
|
entry[1].update(extensions)
|
|
return paths
|
|
|
|
|
|
def _supported_pt_extensions(folder_paths_module: Any) -> set:
|
|
"""ComfyUI's own weight extensions, or an empty set on an unfamiliar core."""
|
|
return set(getattr(folder_paths_module, "supported_pt_extensions", ()) or ())
|
|
|
|
|
|
def register_chatterbox_folders(folder_paths_module: Any) -> list[str]:
|
|
"""Register the ``tts`` and ``chatterbox`` folder keys; return the TTS roots.
|
|
|
|
Idempotent: ComfyUI re-imports a custom node on every startup, and a second
|
|
call adds no duplicate path.
|
|
|
|
Args:
|
|
folder_paths_module: An object exposing ``models_dir``,
|
|
``folder_names_and_paths`` and (optionally)
|
|
``supported_pt_extensions``. The real ``folder_paths`` module in
|
|
production; a fake in tests.
|
|
|
|
Returns:
|
|
Every registered TTS root, in registration order.
|
|
"""
|
|
folder_names_and_paths = folder_paths_module.folder_names_and_paths
|
|
models_dir = os.fspath(folder_paths_module.models_dir)
|
|
|
|
tts_roots = _ensure_entry(
|
|
folder_names_and_paths,
|
|
TTS_FOLDER_KEY,
|
|
_supported_pt_extensions(folder_paths_module) | CHATTERBOX_EXTENSIONS,
|
|
)
|
|
|
|
# Our own root goes last so the roots another node registered keep their
|
|
# indices -- the multi-root case is the normal case, not the exception.
|
|
_append_unique_path(tts_roots, os.path.join(models_dir, TTS_FOLDER_KEY))
|
|
|
|
chatterbox_roots = _ensure_entry(
|
|
folder_names_and_paths,
|
|
CHATTERBOX_FOLDER_KEY,
|
|
_supported_pt_extensions(folder_paths_module) | CHATTERBOX_EXTENSIONS,
|
|
)
|
|
for root in list(tts_roots):
|
|
_append_unique_path(chatterbox_roots, os.path.join(root, CHATTERBOX_SUBDIR))
|
|
|
|
logging.debug(
|
|
"[Chatterbox TTS] folder_registration: %d TTS root(s) registered: %s",
|
|
len(tts_roots),
|
|
", ".join(tts_roots),
|
|
)
|
|
return list(tts_roots)
|
|
|
|
|
|
def get_registered_tts_roots(folder_paths_module: Any) -> list[str]:
|
|
"""Return every TTS root ComfyUI currently knows about, normalised."""
|
|
entry = folder_paths_module.folder_names_and_paths.get(TTS_FOLDER_KEY)
|
|
if not entry:
|
|
return []
|
|
try:
|
|
return [_normalize(path) for path in entry[0]]
|
|
except (TypeError, IndexError):
|
|
return []
|
|
|
|
|
|
def ensure_directory(path: str) -> bool:
|
|
"""``os.makedirs(path, exist_ok=True)``, reporting failure instead of raising.
|
|
|
|
Directory creation is a convenience for the download path. A read-only or
|
|
otherwise unwritable ``models`` tree must not take the whole node down at
|
|
startup, so the failure is logged and ``False`` is returned.
|
|
"""
|
|
try:
|
|
os.makedirs(path, exist_ok=True)
|
|
except OSError as exc:
|
|
logging.warning(
|
|
"[Chatterbox TTS] folder_registration: could not create '%s': %s", path, exc
|
|
)
|
|
return False
|
|
return True
|
|
|
|
|
|
def ensure_chatterbox_directories(tts_roots: Iterable[str]) -> list[str]:
|
|
"""Create ``<tts root>/chatterbox`` for each root; return what succeeded.
|
|
|
|
Split out of :func:`register_chatterbox_folders` on purpose: registration
|
|
only ever mutates ComfyUI's table, while this writes to disk and therefore
|
|
belongs to the download/generation path (batch B8), not to node import.
|
|
"""
|
|
created = []
|
|
for root in tts_roots:
|
|
if not root:
|
|
continue
|
|
target = os.path.join(os.fspath(root), CHATTERBOX_SUBDIR)
|
|
if ensure_directory(target):
|
|
created.append(_normalize(target))
|
|
return created |