haybale-models 0.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. haybale_models-0.3.1/.gitignore +227 -0
  2. haybale_models-0.3.1/PKG-INFO +11 -0
  3. haybale_models-0.3.1/README.md +34 -0
  4. haybale_models-0.3.1/haybale_models/OVERVIEW.md +30 -0
  5. haybale_models-0.3.1/haybale_models/QUICKREF.md +27 -0
  6. haybale_models-0.3.1/haybale_models/__init__.py +44 -0
  7. haybale_models-0.3.1/haybale_models/consumer.py +115 -0
  8. haybale_models-0.3.1/haybale_models/describe.py +111 -0
  9. haybale_models-0.3.1/haybale_models/docs/haybale-models.editor.ModelsEditor.md +35 -0
  10. haybale_models-0.3.1/haybale_models/docs/haybale-models.farmhand.fetch.md +23 -0
  11. haybale_models-0.3.1/haybale_models/docs/haybale-models.farmhand.list.md +28 -0
  12. haybale_models-0.3.1/haybale_models/docs/haybale-models.farmhand.needs.md +24 -0
  13. haybale_models-0.3.1/haybale_models/docs/haybale-models.node.ResolveModelNode.md +33 -0
  14. haybale_models-0.3.1/haybale_models/docs/haybale-models.panel.ModelsSettingsPanel.md +14 -0
  15. haybale_models-0.3.1/haybale_models/docs/haybale-models.setting.ModelsSettings.md +9 -0
  16. haybale_models-0.3.1/haybale_models/docs/haybale-models.state.ModelsState.md +9 -0
  17. haybale_models-0.3.1/haybale_models/docs/haybale-models.type.MODEL_REF.md +19 -0
  18. haybale_models-0.3.1/haybale_models/docs/haybale-models.widget.ModelRefWidget.md +53 -0
  19. haybale_models-0.3.1/haybale_models/download.py +675 -0
  20. haybale_models-0.3.1/haybale_models/editors/__init__.py +0 -0
  21. haybale_models-0.3.1/haybale_models/editors/assisted_dialog.py +180 -0
  22. haybale_models-0.3.1/haybale_models/editors/models_editor.py +1399 -0
  23. haybale_models-0.3.1/haybale_models/farmhands/__init__.py +0 -0
  24. haybale_models-0.3.1/haybale_models/farmhands/model_tools.py +225 -0
  25. haybale_models-0.3.1/haybale_models/filters.py +78 -0
  26. haybale_models-0.3.1/haybale_models/haybale.toml +13 -0
  27. haybale_models-0.3.1/haybale_models/hf.py +386 -0
  28. haybale_models-0.3.1/haybale_models/keys.py +10 -0
  29. haybale_models-0.3.1/haybale_models/matcher.py +50 -0
  30. haybale_models-0.3.1/haybale_models/needs.py +565 -0
  31. haybale_models-0.3.1/haybale_models/nodes/__init__.py +0 -0
  32. haybale_models-0.3.1/haybale_models/nodes/resolve_model.py +66 -0
  33. haybale_models-0.3.1/haybale_models/panels/__init__.py +1 -0
  34. haybale_models-0.3.1/haybale_models/panels/models_settings_panel.py +32 -0
  35. haybale_models-0.3.1/haybale_models/pool.py +113 -0
  36. haybale_models-0.3.1/haybale_models/py.typed +0 -0
  37. haybale_models-0.3.1/haybale_models/receive.py +150 -0
  38. haybale_models-0.3.1/haybale_models/resolver.py +418 -0
  39. haybale_models-0.3.1/haybale_models/settings/__init__.py +0 -0
  40. haybale_models-0.3.1/haybale_models/settings/models_settings.py +17 -0
  41. haybale_models-0.3.1/haybale_models/signals.py +18 -0
  42. haybale_models-0.3.1/haybale_models/sources/__init__.py +48 -0
  43. haybale_models-0.3.1/haybale_models/sources/base.py +195 -0
  44. haybale_models-0.3.1/haybale_models/sources/caches.py +124 -0
  45. haybale_models-0.3.1/haybale_models/sources/folder.py +143 -0
  46. haybale_models-0.3.1/haybale_models/sources/hf.py +110 -0
  47. haybale_models-0.3.1/haybale_models/sources/https.py +132 -0
  48. haybale_models-0.3.1/haybale_models/sources/registry.py +97 -0
  49. haybale_models-0.3.1/haybale_models/state/__init__.py +0 -0
  50. haybale_models-0.3.1/haybale_models/state/models_state.py +179 -0
  51. haybale_models-0.3.1/haybale_models/status.py +48 -0
  52. haybale_models-0.3.1/haybale_models/store/__init__.py +1 -0
  53. haybale_models-0.3.1/haybale_models/store/index.py +302 -0
  54. haybale_models-0.3.1/haybale_models/store/layout.py +118 -0
  55. haybale_models-0.3.1/haybale_models/store/records.py +125 -0
  56. haybale_models-0.3.1/haybale_models/types/__init__.py +0 -0
  57. haybale_models-0.3.1/haybale_models/types/model_ref.py +74 -0
  58. haybale_models-0.3.1/haybale_models/upload_route.py +176 -0
  59. haybale_models-0.3.1/haybale_models/value/__init__.py +5 -0
  60. haybale_models-0.3.1/haybale_models/value/model_ref.py +83 -0
  61. haybale_models-0.3.1/haybale_models/widgets/__init__.py +0 -0
  62. haybale_models-0.3.1/haybale_models/widgets/model_ref_widget.py +657 -0
  63. haybale_models-0.3.1/pyproject.toml +24 -0
@@ -0,0 +1,227 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # The MANIFEST/*.manifest packaging-artifact rules above are unanchored and
36
+ # (on a case-insensitive filesystem) also match our real, tracked
37
+ # haywire_studio/share/manifest/ package — un-ignore it explicitly.
38
+ !packages/haywire-studio/src/haywire_studio/share/manifest/
39
+
40
+ # Installer logs
41
+ pip-log.txt
42
+ pip-delete-this-directory.txt
43
+
44
+ # Unit test / coverage reports
45
+ htmlcov/
46
+ .tox/
47
+ .nox/
48
+ .coverage
49
+ .coverage.*
50
+ .cache
51
+ nosetests.xml
52
+ coverage.xml
53
+ *.cover
54
+ *.py,cover
55
+ .hypothesis/
56
+ .pytest_cache/
57
+ cover/
58
+
59
+ # Translations
60
+ *.mo
61
+ *.pot
62
+
63
+ # Django stuff:
64
+ *.log
65
+ local_settings.py
66
+ db.sqlite3
67
+ db.sqlite3-journal
68
+
69
+ # Flask stuff:
70
+ instance/
71
+ .webassets-cache
72
+
73
+ # Scrapy stuff:
74
+ .scrapy
75
+
76
+ # Sphinx documentation
77
+ docs/_build/
78
+
79
+ # PyBuilder
80
+ .pybuilder/
81
+ target/
82
+
83
+ # Jupyter Notebook
84
+ .ipynb_checkpoints
85
+
86
+ # IPython
87
+ profile_default/
88
+ ipython_config.py
89
+
90
+ # pyenv
91
+ # For a library or package, you might want to ignore these files since the code is
92
+ # intended to run in multiple environments; otherwise, check them in:
93
+ # .python-version
94
+
95
+ # pipenv
96
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
97
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
98
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
99
+ # install all needed dependencies.
100
+ #Pipfile.lock
101
+
102
+ # UV
103
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
104
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
105
+ # commonly ignored for libraries.
106
+ #uv.lock
107
+
108
+ # poetry
109
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
110
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
111
+ # commonly ignored for libraries.
112
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
113
+ #poetry.lock
114
+
115
+ # pdm
116
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
117
+ #pdm.lock
118
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
119
+ # in version control.
120
+ # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
121
+ .pdm.toml
122
+ .pdm-python
123
+ .pdm-build/
124
+
125
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
126
+ __pypackages__/
127
+
128
+ # Celery stuff
129
+ celerybeat-schedule
130
+ celerybeat.pid
131
+
132
+ # SageMath parsed files
133
+ *.sage.py
134
+
135
+ # Environments
136
+ **/.env
137
+ .venv
138
+ env/
139
+ venv/
140
+ ENV/
141
+ env.bak/
142
+ venv.bak/
143
+
144
+ # Spyder project settings
145
+ .spyderproject
146
+ .spyproject
147
+
148
+ # Rope project settings
149
+ .ropeproject
150
+
151
+ # mkdocs documentation
152
+ /site
153
+
154
+ # mypy
155
+ .mypy_cache/
156
+ .dmypy.json
157
+ dmypy.json
158
+
159
+ # Pyre type checker
160
+ .pyre/
161
+
162
+ # pytype static type analyzer
163
+ .pytype/
164
+
165
+ # Cython debug symbols
166
+ cython_debug/
167
+
168
+ # PyCharm
169
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
170
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
171
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
172
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
173
+ .idea/
174
+
175
+ # Abstra
176
+ # Abstra is an AI-powered process automation framework.
177
+ # Ignore directories containing user credentials, local state, and settings.
178
+ # Learn more at https://abstra.io/docs
179
+ .abstra/
180
+
181
+ # Visual Studio Code
182
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
183
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
184
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
185
+ # you could uncomment the following to ignore the enitre vscode folder
186
+ .vscode/
187
+
188
+ # Ruff stuff:
189
+ .ruff_cache/
190
+
191
+ # PyPI configuration file
192
+ .pypirc
193
+
194
+ # Cursor
195
+ # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
196
+ # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
197
+ # refer to https://docs.cursor.com/context/ignore-files
198
+ .cursorignore
199
+ .cursorindexingignore
200
+
201
+ .DS_Store
202
+ saves/*.*
203
+
204
+ # Dev-mode graphs (local testing only, not part of the repo)
205
+ graphs/*.json
206
+ # MkDocs build output
207
+ site/
208
+
209
+ # Baked docs for the haywire-core wheel (generated by scripts/bake_docs.py;
210
+ # force-included into the wheel at build time). The *contents* are never
211
+ # committed, but the directory itself is kept via a committed .gitkeep so the
212
+ # hatchling force-include target exists in a fresh checkout — otherwise every
213
+ # editable install (`uv sync`) fails the build on a missing force-include path.
214
+ packages/haywire-core/src/haywire/_baked_docs/*
215
+ !packages/haywire-core/src/haywire/_baked_docs/.gitkeep
216
+
217
+ # Local git worktrees (claude-code workflow)
218
+ .worktrees/
219
+
220
+ # Haywire project config (user-local, not repo state)
221
+ .haywire/
222
+
223
+ # Local symlinks to external dev projects (machine-specific paths)
224
+ barn/haybale-visiongraph
225
+ barn/haybale-spandrel
226
+
227
+ assets/*
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.5
2
+ Name: haybale-models
3
+ Version: 0.3.1
4
+ Summary: Model references, local model store, downloads and graph needs-list
5
+ License: MIT
6
+ Keywords: models
7
+ Requires-Python: >=3.12
8
+ Requires-Dist: haybale-graph-editor>=0.3.1
9
+ Requires-Dist: haywire-core>=0.3.1
10
+ Requires-Dist: httpx>=0.28
11
+ Requires-Dist: huggingface-hub>=2.4
@@ -0,0 +1,34 @@
1
+ # Models
2
+
3
+ <!-- marketstall:share-url:start -->
4
+ *Subscribe URL not yet published — run `haywire share --save`.*
5
+ <!-- marketstall:share-url:end -->
6
+
7
+ Model references, local model store, downloads and graph needs-list
8
+
9
+ ## Nodes
10
+ ### Models
11
+ - **Resolve Model** — Gives the file of a model on this machine, with its sha256 and size
12
+
13
+ ## Types
14
+ - **Model** — A model, named by a catalog entry or by a file (url and sha256). Resolve Model turns it into a path on disk.
15
+
16
+ ## Widgets
17
+ - **ModelRefWidget** — Pick a model
18
+
19
+ ## Settings
20
+ - **Models** — Model download behaviour
21
+
22
+ ## Farmhands
23
+ - **Fetch models** — Queue downloads: everything an open graph needs, or catalog entries by key (admin).
24
+ - **List models** — Catalog entries with per-variant status, indexed local files and download jobs.
25
+ - **Models a graph needs** — The models an open graph references and does not have locally.
26
+
27
+ ## States
28
+ - **Models** — Model volumes, file index, download queue and the pool of loaded models
29
+
30
+ ## Panels
31
+ - **Models** — Model download behaviour: how many files download in parallel.
32
+
33
+ ## Editors
34
+ - **Models** — Model files on this machine and where they come from
@@ -0,0 +1,30 @@
1
+ # Models
2
+
3
+ Model references, local model store, downloads and graph needs-list
4
+
5
+ ## Nodes
6
+ ### Models
7
+ - **Resolve Model** — Gives the file of a model on this machine, with its sha256 and size
8
+
9
+ ## Types
10
+ - **Model** — A model, named by a catalog entry or by a file (url and sha256). Resolve Model turns it into a path on disk.
11
+
12
+ ## Widgets
13
+ - **ModelRefWidget** — Pick a model
14
+
15
+ ## Settings
16
+ - **Models** — Model download behaviour
17
+
18
+ ## Farmhands
19
+ - **Fetch models** — Queue downloads: everything an open graph needs, or catalog entries by key (admin).
20
+ - **List models** — Catalog entries with per-variant status, indexed local files and download jobs.
21
+ - **Models a graph needs** — The models an open graph references and does not have locally.
22
+
23
+ ## States
24
+ - **Models** — Model volumes, file index, download queue and the pool of loaded models
25
+
26
+ ## Panels
27
+ - **Models** — Model download behaviour: how many files download in parallel.
28
+
29
+ ## Editors
30
+ - **Models** — Model files on this machine and where they come from
@@ -0,0 +1,27 @@
1
+ # haybale-models — component index (v0.3.1)
2
+
3
+ ## node
4
+ - `haybale-models:node:ResolveModelNode` — Resolve Model — Gives the file of a model on this machine, with its sha256 and size _tags: model, resolve, file, path, weights, catalog, sha256_
5
+
6
+ ## type
7
+ - `haybale-models:type:MODEL_REF` — Model — A model, named by a catalog entry or by a file (url and sha256). Resolve Model turns it into a path on disk.
8
+
9
+ ## widget
10
+ - `haybale-models:widget:ModelRefWidget` — ModelRefWidget — Pick a model
11
+
12
+ ## setting
13
+ - `haybale-models:setting:ModelsSettings` — Models — Model download behaviour
14
+
15
+ ## farmhand
16
+ - `haybale-models:farmhand:fetch` — Fetch models — Queue downloads: everything an open graph needs, or catalog entries by key (admin).
17
+ - `haybale-models:farmhand:list` — List models — Catalog entries with per-variant status, indexed local files and download jobs.
18
+ - `haybale-models:farmhand:needs` — Models a graph needs — The models an open graph references and does not have locally.
19
+
20
+ ## state
21
+ - `haybale-models:state:ModelsState` — Models — Model volumes, file index, download queue and the pool of loaded models
22
+
23
+ ## panel
24
+ - `haybale-models:panel:ModelsSettingsPanel` — Models — Model download behaviour: how many files download in parallel.
25
+
26
+ ## editor
27
+ - `haybale-models:editor:ModelsEditor` — Models — Model files on this machine and where they come from
@@ -0,0 +1,44 @@
1
+ """Haybale-Models: model references, local model store, downloads and needs-list.
2
+
3
+ Component folders (registered in ``register_components``):
4
+ - ``state``: ``ModelsState`` with the two model volumes.
5
+ - ``settings``, ``types``, ``widgets``, ``nodes``, ``editors``, ``farmhands``, ``panels``.
6
+
7
+ Never imports ``torch``: variant choice comes from the consuming node's settings.
8
+ """
9
+
10
+ from pathlib import Path
11
+
12
+ from haywire.core.farmhand import FarmhandRegistry
13
+ from haywire.core.library.base import BaseLibrary
14
+ from haywire.core.library.decorator import library
15
+ from haywire.core.node.registry import NodeRegistry
16
+ from haywire.core.settings.registry import SettingsRegistry
17
+ from haywire.core.state import LibraryStateRegistry
18
+ from haywire.core.types.registry import TypeRegistry
19
+ from haywire.ui.editor.registry import EditorTypeRegistry
20
+ from haywire.ui.panel.registry import PanelRegistry
21
+ from haywire.ui.widget.registry import WidgetRegistry
22
+
23
+
24
+ @library(
25
+ file_watcher=True,
26
+ )
27
+ class Library(BaseLibrary):
28
+ """Models library — model references, store and downloads."""
29
+
30
+ def register_components(self):
31
+ """Register the component folders with their registries."""
32
+ base_path = Path(__file__).parent
33
+
34
+ self.add_folder_to_registry(folder_path=str(base_path / "state"), registry_cls=LibraryStateRegistry)
35
+ self.add_folder_to_registry(folder_path=str(base_path / "settings"), registry_cls=SettingsRegistry)
36
+ self.add_folder_to_registry(folder_path=str(base_path / "types"), registry_cls=TypeRegistry)
37
+ self.add_folder_to_registry(folder_path=str(base_path / "widgets"), registry_cls=WidgetRegistry)
38
+ self.add_folder_to_registry(folder_path=str(base_path / "nodes"), registry_cls=NodeRegistry)
39
+ self.add_folder_to_registry(folder_path=str(base_path / "editors"), registry_cls=EditorTypeRegistry)
40
+ self.add_folder_to_registry(folder_path=str(base_path / "farmhands"), registry_cls=FarmhandRegistry)
41
+ self.add_folder_to_registry(folder_path=str(base_path / "panels"), registry_cls=PanelRegistry)
42
+
43
+ def validate(self) -> bool:
44
+ return True
@@ -0,0 +1,115 @@
1
+ """Helpers for node authors: resolve a MODEL_REF port and hold its loaded model per run."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable, Hashable
6
+ from typing import TYPE_CHECKING, TypeVar
7
+
8
+ from haywire.core.di.context import get_library_state_container
9
+ from haywire.core.node.node_warning import NodeWarning
10
+
11
+ from haybale_models.matcher import Preference
12
+ from haybale_models.resolver import ModelMissing, ResolvedModel, resolve
13
+ from haybale_models.state.models_state import ModelsState
14
+ from haybale_models.value.model_ref import ModelRef
15
+
16
+ if TYPE_CHECKING:
17
+ from haywire.core.node.base import BaseNode
18
+
19
+ __all__ = [
20
+ "KIND",
21
+ "MISSING_WARNING",
22
+ "ModelMissing",
23
+ "acquire_port",
24
+ "models_state",
25
+ "preference_for",
26
+ "release_node",
27
+ "resolve_port",
28
+ ]
29
+
30
+ T = TypeVar("T")
31
+
32
+ KIND = "model"
33
+ MISSING_WARNING = NodeWarning(message="Model missing — Show in Models", source_version=None, kind=KIND)
34
+
35
+
36
+ def models_state() -> ModelsState:
37
+ """The app-wide :class:`ModelsState`.
38
+
39
+ Raises:
40
+ RuntimeError: If haybale-models is not enabled.
41
+ """
42
+ state = get_library_state_container().get(ModelsState)
43
+ if state is None:
44
+ raise RuntimeError("haybale-models is not enabled")
45
+ return state
46
+
47
+
48
+ def preference_for(node: BaseNode, port_id: str) -> Preference:
49
+ """The variant preference for *port_id*.
50
+
51
+ The node's ``model_preference(port_id)`` if it defines one, else the port's ``prefer``
52
+ property, else ``[{}]``.
53
+ """
54
+ hook = getattr(node, "model_preference", None)
55
+ if callable(hook):
56
+ return hook(port_id) # type: ignore[no-any-return]
57
+ prefer = node.ports[port_id].widget_config.get("properties", {}).get("prefer")
58
+ return list(prefer) if prefer else [{}]
59
+
60
+
61
+ def resolve_port(node: BaseNode, port_id: str) -> ResolvedModel:
62
+ """Resolve the model on *port_id* and set the node's ``"model"`` warnings.
63
+
64
+ All ``MODEL_REF`` ports of one node share a single ``"model"`` warning list, so with
65
+ several ports the last call wins. Key the list per port when a two-port consumer lands.
66
+
67
+ Raises:
68
+ ModelMissing: If no local file matches the reference.
69
+ """
70
+ state = models_state()
71
+ value = node.ports[port_id].get_value()
72
+ ref = value if isinstance(value, ModelRef) else ModelRef.from_dict(value or {})
73
+ try:
74
+ resolved = resolve(
75
+ ref,
76
+ preference_for(node, port_id),
77
+ catalog=state.catalog,
78
+ index=state.index,
79
+ sources=state.sources,
80
+ )
81
+ except ModelMissing:
82
+ node.wrapper.replace_warnings(KIND, [] if ref.is_empty else [MISSING_WARNING])
83
+ raise
84
+ node.wrapper.replace_warnings(KIND, _warnings(resolved))
85
+ return resolved
86
+
87
+
88
+ def _warnings(resolved: ResolvedModel) -> list[NodeWarning]:
89
+ messages: list[str] = []
90
+ if resolved.better_unfetched is not None:
91
+ variant = resolved.better_unfetched.variant_name()
92
+ messages.append(f"Preferred variant {variant} is not downloaded — Show in Models")
93
+ if resolved.ref.is_file and not resolved.ref.sha256:
94
+ messages.append("Not pinned — use the pin on the card")
95
+ return [NodeWarning(message=m, source_version=None, kind=KIND) for m in messages]
96
+
97
+
98
+ def acquire_port(
99
+ node: BaseNode,
100
+ port_id: str,
101
+ *,
102
+ variant_key: Hashable,
103
+ load: Callable[[ResolvedModel], T],
104
+ release: Callable[[T], None],
105
+ ) -> T:
106
+ """Resolve *port_id* and hold its loaded model for *node*; nodes share one load per key."""
107
+ resolved = resolve_port(node, port_id)
108
+ return models_state().pool.acquire(
109
+ (resolved.key, variant_key), node, load=lambda: load(resolved), release=release
110
+ )
111
+
112
+
113
+ def release_node(node: BaseNode) -> None:
114
+ """Drop *node*'s holds. Call from ``on_shutdown`` and, as a backstop, ``on_teardown``."""
115
+ models_state().pool.release(node)
@@ -0,0 +1,111 @@
1
+ """What a catalog entry is, in words: a one-line summary, a plain-text description, and search text.
2
+
3
+ Used wherever a person chooses between models (the model picker, the Models editor),
4
+ so both describe an entry the same way.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import re
10
+ from typing import TYPE_CHECKING
11
+
12
+ if TYPE_CHECKING:
13
+ from haywire.core.catalog import CatalogEntry
14
+
15
+ from haybale_models.filters import COMMERCIAL, NON_COMMERCIAL
16
+
17
+ _NOT_NON_COMMERCIAL = re.compile(r"(?<!non-)commercial")
18
+
19
+ __all__ = ["entry_description", "entry_search_text", "entry_summary", "search_matches"]
20
+
21
+ #: Tags shown in a summary line; the rest are counted (``+2``).
22
+ SUMMARY_TAGS = 4
23
+
24
+ #: Longest description shown in a tip, in characters; longer text ends in ``…``.
25
+ DESCRIPTION_LIMIT = 400
26
+
27
+ _LINK = re.compile(r"\[([^\]]+)\]\([^)]*\)")
28
+ _MARKUP = re.compile(r"[*_`#>]+")
29
+
30
+
31
+ def entry_summary(entry: CatalogEntry, *, scale: bool = True) -> str:
32
+ """Return what tells *entry* apart at a glance, such as ``4x · ESRGAN · anime, photo +2``.
33
+
34
+ Scale, architecture and the first tags, each left out when the entry has none.
35
+ ``scale=False`` leaves the scale out, for a place that already shows it.
36
+ """
37
+ parts: list[str] = []
38
+ if scale and entry.scale:
39
+ parts.append(f"{entry.scale}x")
40
+ if entry.architecture:
41
+ parts.append(entry.architecture)
42
+ if entry.tags:
43
+ shown = ", ".join(entry.tags[:SUMMARY_TAGS])
44
+ rest = len(entry.tags) - SUMMARY_TAGS
45
+ parts.append(f"{shown} +{rest}" if rest > 0 else shown)
46
+ return " · ".join(parts)
47
+
48
+
49
+ def entry_description(entry: CatalogEntry) -> str:
50
+ """Return *entry*'s description as plain text for a tip, with purpose, author and licence below.
51
+
52
+ Markdown links keep their text, other markup is dropped, and a description longer
53
+ than ``DESCRIPTION_LIMIT`` characters is cut at a word and ends in ``…``.
54
+ """
55
+ text = _plain(entry.description)
56
+ if len(text) > DESCRIPTION_LIMIT:
57
+ text = text[:DESCRIPTION_LIMIT].rsplit(" ", 1)[0] + " …"
58
+ facts = []
59
+ if entry.purpose:
60
+ facts.append("Purpose: " + ", ".join(entry.purpose))
61
+ if entry.author:
62
+ facts.append("By " + entry.author)
63
+ if licence := entry_licence_text(entry):
64
+ facts.append("Licence: " + licence)
65
+ return "\n".join(part for part in (text, " · ".join(facts)) if part)
66
+
67
+
68
+ def entry_licence_text(entry: CatalogEntry) -> str:
69
+ """Return *entry*'s licence, followed by ``non-commercial`` unless its author states otherwise.
70
+
71
+ Empty for a commercial entry without a licence.
72
+ """
73
+ parts = [entry.license, "" if entry.commercial else NON_COMMERCIAL]
74
+ return " · ".join(part for part in parts if part)
75
+
76
+
77
+ def entry_search_text(entry: CatalogEntry) -> str:
78
+ """Return the lower-case text a search over *entry* matches; see :func:`search_matches`.
79
+
80
+ Its name, scale (``4x``), architecture, purpose, tags, licence, commercial use and description.
81
+ """
82
+ words = [
83
+ entry.name,
84
+ f"{entry.scale}x" if entry.scale else "",
85
+ entry.architecture,
86
+ *entry.purpose,
87
+ *entry.tags,
88
+ entry.license,
89
+ COMMERCIAL if entry.commercial else NON_COMMERCIAL,
90
+ _plain(entry.description),
91
+ ]
92
+ return " ".join(w for w in words if w).lower()
93
+
94
+
95
+ def search_matches(query: str, text: str) -> bool:
96
+ """Return whether every word of *query* appears somewhere in *text*, ignoring case.
97
+
98
+ A word may be part of a longer one, in any order. An empty query matches everything. The word
99
+ ``commercial`` does not match ``non-commercial``.
100
+ """
101
+ text = text.lower()
102
+ return all(
103
+ _NOT_NON_COMMERCIAL.search(text) if word == COMMERCIAL else word in text
104
+ for word in query.lower().split()
105
+ )
106
+
107
+
108
+ def _plain(markdown: str) -> str:
109
+ text = _LINK.sub(r"\1", markdown or "")
110
+ text = _MARKUP.sub("", text)
111
+ return " ".join(text.split())
@@ -0,0 +1,35 @@
1
+ # Models
2
+
3
+ `haybale-models:editor:ModelsEditor` · kind: editor
4
+
5
+ Model files on this machine and where they come from
6
+
7
+ ## Details
8
+
9
+ - **default_slot**: `action`
10
+ - **opens**: `OpenBehavior.REQUIRED`
11
+ - **order**: `100`
12
+
13
+ ## Notes
14
+
15
+ Lists the model catalog, the files in the Models volume and the Hugging Face files in use.
16
+
17
+ The **Filters** icon in the header opens the filter section. Its **Show:** row picks the sections
18
+ (**Catalog**, **Models**, **Huggingface**). Its search toggle opens a field that narrows every
19
+ section: each word must appear, in any order, and scale, licence and `commercial` count too. Its
20
+ **+** adds a filter on library, purpose, tags, architecture, scale, commercial use or status, which
21
+ narrows the catalog. Closing it switches them all off, so a closed section always means the full
22
+ list, and reopening it brings them back.
23
+ The **Needed** section is always shown: per open graph, the models it lacks; **Fetch all**
24
+ downloads them. Each section header counts what the section lists; hover it for the whole
25
+ volume's size.
26
+
27
+ A catalog row reads `non-commercial` unless its author states the model may be used commercially.
28
+ Each catalog resource shows its status: `local`, `HF cache`, `downloading`, `failed` or
29
+ `not downloaded`. **Models** lists the files in the Models volume that no catalog entry
30
+ occupies. **Huggingface** lists the cached files that catalog entries or open graphs use,
31
+ and what uses each; the Volumes editor shows where the cache lives. Download, Cancel and
32
+ Remove need admin access; a removal names the open graphs that use the file and the bytes
33
+ it frees. Cancel is offered only while a download can still be stopped: a Hugging Face
34
+ download, once started, runs to the end. Opening the editor and **Rescan** index the Models
35
+ volume on a background thread.
@@ -0,0 +1,23 @@
1
+ # Fetch models
2
+
3
+ `haybale-models:farmhand:fetch` · kind: farmhand
4
+
5
+ Queue downloads: everything an open graph needs, or catalog entries by key (admin).
6
+
7
+ ## Agent Instructions
8
+
9
+ Queues the downloads and returns their job ids; progress is in models_list jobs. Pass binding_id to fetch everything that graph needs, or keys=[<catalog key>, ...] to fetch the preferred variant of each entry. Needs admin access.
10
+
11
+ ## Details
12
+
13
+ - **input_schema**: `{'type': 'object', 'properties': {'binding_id': {'type': 'string', 'default': None}, 'keys': {'type': 'array', 'items': {'type': 'string'}, 'default': None}}, 'required': []}`
14
+ - **annotations**: `{'read_only_hint': False, 'destructive_hint': False, 'idempotent_hint': False, 'open_world_hint': True}`
15
+
16
+ ## Notes
17
+
18
+ Queues downloads of missing models. Needs ADMIN access.
19
+
20
+ Pass `binding_id` to fetch everything that open graph needs, or `keys` to fetch the
21
+ preferred variant of each catalog entry. Returns the job ids at once; the files
22
+ arrive in the background, so call `models_list` to follow the jobs. Downloads use
23
+ network and disk: call it only when the user wants the models.