feat(other-models): announce the feature only when folders are available

Other Models management is opt-in and its folders come from
folder_paths.get_folder_paths(). In plugin mode ComfyUI registers vae,
upscale_models, text_encoders, clip_vision and controlnet out of the box, so
enabling the feature works immediately. Standalone only knows the keys present
in settings.json.folder_paths, and that file is edited by hand - there is no UI
for those keys - so a standalone user who followed the announcement banner
reached "Enable Other Models" and then an empty page.

Gate the announcement on the capability instead of on how the process was
started:

- Config.get_other_models_availability() probes every canonical other key
  (legacy clip collapses into text_encoders where the host exposes
  map_legacy) and reports which sub_types resolve to a folder that exists on
  disk. It deliberately ignores enable_other_models: the question is "could
  this work here at all?". An empty folder counts, because CivitAI downloads
  can target it.
- /api/lm/settings exposes it as the derived, non-persisted
  other_models_paths_available flag; a probe failure yields null and the
  banner fails open.
- BannerService only registers the announcement when the flag is not false.
  `=== false` (not falsy) keeps a cached/older payload working, and nothing is
  written to dismissed_banners, so the banner can return once folders exist.
- The Other page grows an "enabled but nothing to scan" empty state driven by
  config.other_roots, showing the settings.json snippet for standalone and a
  pointer to ComfyUI model paths otherwise, plus an Open Settings action. It
  also covers the corner where only a non-default sub_type has a folder.

Translate the six other.noPaths.* keys into all nine locales and record the
new "folder key" / "on disk" terminology in the i18n guidelines.

Backend tests and pytest tests/i18n could not run in this environment (no
pytest/platformdirs); the probe was exercised against a stubbed folder_paths.
Frontend: 120 files / 1101 JS tests passed.
This commit is contained in:
Will Miao
2026-09-13 21:47:16 +08:00
parent adeb40bfff
commit 84146b62fd
22 changed files with 357 additions and 7 deletions
+71
View File
@@ -414,3 +414,74 @@ class TestOtherRootsWiring:
config._rebuild_preview_roots()
assert config.is_preview_path_allowed(str(vae_dir / "model.preview.png"))
class TestOtherModelsAvailability:
"""Config.get_other_models_availability ignores the opt-in toggle.
It answers "could Other Models work here at all?", which the settings
payload and the announcement banner use to avoid promising a page that
cannot list anything.
"""
def _stub_folder_paths(self, monkeypatch, mapping):
def get_folder_paths(key):
value = mapping.get(key, [])
return [value] if isinstance(value, str) else list(value)
monkeypatch.setattr(
config_module.folder_paths, "get_folder_paths", get_folder_paths
)
# No host alias rewriting: every key stays independently queryable,
# which is what the standalone mock does.
monkeypatch.delattr(config_module.folder_paths, "map_legacy", raising=False)
def test_reports_available_when_a_folder_exists(self, monkeypatch, tmp_path):
vae_dir = tmp_path / "vae"
vae_dir.mkdir()
self._stub_folder_paths(monkeypatch, {"vae": str(vae_dir)})
# The feature stays off on purpose: availability must not depend on it.
get_settings_manager().set("enable_other_models", False)
availability = _make_config().get_other_models_availability()
assert availability["available"] is True
assert availability["sub_types"] == {"vae": [_normalize(str(vae_dir))]}
def test_counts_an_empty_but_existing_folder(self, monkeypatch, tmp_path):
vae_dir = tmp_path / "vae"
vae_dir.mkdir()
self._stub_folder_paths(monkeypatch, {"vae": str(vae_dir)})
availability = _make_config().get_other_models_availability()
assert availability["available"] is True
def test_ignores_missing_folders(self, monkeypatch, tmp_path):
self._stub_folder_paths(
monkeypatch, {"vae": str(tmp_path / "does-not-exist")}
)
availability = _make_config().get_other_models_availability()
assert availability == {"available": False, "sub_types": {}}
def test_reports_unavailable_without_any_configuration(self, monkeypatch):
self._stub_folder_paths(monkeypatch, {})
availability = _make_config().get_other_models_availability()
assert availability == {"available": False, "sub_types": {}}
def test_merges_legacy_clip_key_into_text_encoder(self, monkeypatch, tmp_path):
clip_dir = tmp_path / "clip"
clip_dir.mkdir()
self._stub_folder_paths(monkeypatch, {"clip": [str(clip_dir)]})
availability = _make_config().get_other_models_availability()
assert availability["available"] is True
assert availability["sub_types"] == {
"text_encoder": [_normalize(str(clip_dir))]
}