inline-core 1.2.2__tar.gz → 1.2.3__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.
- {inline_core-1.2.2 → inline_core-1.2.3}/CLAUDE.md +3 -6
- {inline_core-1.2.2 → inline_core-1.2.3}/PKG-INFO +3 -4
- {inline_core-1.2.2 → inline_core-1.2.3}/README.md +2 -3
- {inline_core-1.2.2 → inline_core-1.2.3}/pyproject.toml +1 -1
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/extensions/fetch.py +8 -2
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/server/__main__.py +6 -1
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/studio/config.py +0 -1
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/studio/frames.py +4 -17
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/studio/graph_build.py +13 -2
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/studio/handlers.py +2 -8
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/studio/models.py +42 -59
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/studio/moodboard.py +8 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/studio/store.py +0 -8
- {inline_core-1.2.2 → inline_core-1.2.3}/tests/test_extension_install.py +27 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/tests/test_studio_frames.py +3 -2
- {inline_core-1.2.2 → inline_core-1.2.3}/tests/test_studio_generation.py +25 -1
- inline_core-1.2.3/tests/test_studio_models.py +90 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/tests/test_studio_rpc.py +0 -3
- {inline_core-1.2.2 → inline_core-1.2.3}/tests/test_studio_store.py +2 -5
- inline_core-1.2.2/src/inline_core/importer/__init__.py +0 -1
- inline_core-1.2.2/src/inline_core/importer/comfy.py +0 -162
- inline_core-1.2.2/tests/test_comfy_import.py +0 -80
- inline_core-1.2.2/tests/test_studio_models.py +0 -84
- {inline_core-1.2.2 → inline_core-1.2.3}/.gitignore +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/.python-version +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/main.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/scripts/reference.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/__init__.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/components/__init__.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/components/conditioning.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/components/interfaces.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/config.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/device/__init__.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/device/auto.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/device/detect.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/device/memory.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/device/policy.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/device/types.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/errors.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/extensions/__init__.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/extensions/api.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/extensions/constraints.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/extensions/handlers.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/extensions/importer.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/extensions/install.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/extensions/loader.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/extensions/manifest.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/extensions/models.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/extensions/paths.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/extensions/resolve.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/extensions/scanner.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/extensions/state.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/extensions/tools.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/graph/__init__.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/graph/cache.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/graph/descriptor.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/graph/executor.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/graph/loader_runners.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/graph/primitives.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/graph/registry.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/graph/runners.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/graph/schema.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/graph/topo.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/graph/validate.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/media.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/models/__init__.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/models/catalog.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/models/loaders.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/models/requirements.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/models/sampling.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/models/zimage/__init__.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/models/zimage/primitives.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/models/zimage/provider.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/models/zimage/requirements.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/models/zimage/runner.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/parallel/__init__.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/parallel/config.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/parallel/group.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/parallel/launch.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/parallel/protocol.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/parallel/registry.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/parallel/worker.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/runtime/__init__.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/runtime/context.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/runtime/file_store.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/runtime/progress.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/runtime/run.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/runtime/store.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/sampling/__init__.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/sampling/batch.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/server/__init__.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/server/app.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/server/assets.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/server/bootstrap.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/server/frontend.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/server/manager.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/server/rpc.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/server/run_store.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/server/serialize.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/studio/__init__.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/studio/assets.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/studio/fal.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/studio/generation.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/studio/peaks.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/studio/schema.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/studio/timeline/__init__.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/studio/timeline/compose.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/studio/timeline/ffmpeg.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/studio/timeline/render.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/studio/timeline/resolve.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/src/inline_core/takes.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/tests/helpers.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/tests/test_cache.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/tests/test_catalog.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/tests/test_config.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/tests/test_device_detect.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/tests/test_executor.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/tests/test_extension_api.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/tests/test_extension_manifest.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/tests/test_extension_resolve.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/tests/test_extension_scanner.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/tests/test_extension_spine.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/tests/test_extension_state.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/tests/test_file_store.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/tests/test_frontend_serving.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/tests/test_hidden_nodes.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/tests/test_loader_runners.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/tests/test_loaders.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/tests/test_memory_policy.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/tests/test_model_requirements.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/tests/test_parallel_group.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/tests/test_primitives.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/tests/test_rpc_bridge.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/tests/test_run_store.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/tests/test_sampling.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/tests/test_schema.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/tests/test_server.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/tests/test_studio_assets.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/tests/test_studio_fal.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/tests/test_studio_moodboard.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/tests/test_studio_peaks.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/tests/test_studio_schema.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/tests/test_studio_timeline.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/tests/test_take_bytes.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/tests/test_topo.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/tests/test_validate.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/tests/test_xfuser_sampler.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/tests/test_zimage_primitives.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/tests/test_zimage_resolve.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/tests/test_zimage_runner.py +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/uv.lock +0 -0
- {inline_core-1.2.2 → inline_core-1.2.3}/webui.sh +0 -0
|
@@ -82,8 +82,6 @@ HTTP/WS → server/app.py → RunManager → Executor → Registry (desc
|
|
|
82
82
|
- **`runtime/`** - `context.py` (`ExecutionContext`, `CancelToken`), `run.py` (`RunState`),
|
|
83
83
|
`progress.py` (events + emitters), `store.py` / `file_store.py` (`TakeStore`: owns take bytes/hash/
|
|
84
84
|
uri).
|
|
85
|
-
- **`importer/`** - `comfy.py`: best-effort ComfyUI-workflow → Inline-Core-graph mapping. All ComfyUI
|
|
86
|
-
format knowledge lives here.
|
|
87
85
|
- **`config.py`** - all env config, small and explicit. **`takes.py`**, **`media.py`**, **`errors.py`**
|
|
88
86
|
- domain primitives (`Take`/`AssetRef`, `MediaKind`, the error hierarchy).
|
|
89
87
|
|
|
@@ -200,13 +198,13 @@ real codec that moves tensors lives with the model runner.
|
|
|
200
198
|
subpackages import torch/diffusers at module top **on purpose**: an absent extra makes the import
|
|
201
199
|
raise, and `server/bootstrap.py` skips that model best-effort so a core install still boots and
|
|
202
200
|
serves source nodes. Never import a heavy dep at package top level outside a runner subpackage.
|
|
203
|
-
- **Engine isolation.** All
|
|
204
|
-
|
|
201
|
+
- **Engine isolation.** All xDiT/worker knowledge lives behind `parallel/` and the sampler seam.
|
|
202
|
+
Don't scatter it.
|
|
205
203
|
- **Bring-your-own models.** Nothing is downloaded by the engine. The catalog scans; the user places
|
|
206
204
|
files. A model picker is a `SELECT` param with `options_from="<category>"`.
|
|
207
205
|
- **Tests (pytest).** Cover the logic that matters: graph validate/topo/executor/cache, the catalog
|
|
208
206
|
scan, the run store + server contract, the device/memory policy, the parallel group + xfuser seam,
|
|
209
|
-
|
|
207
|
+
and each model runner (import-guarded, no GPU needed). See `tests/`.
|
|
210
208
|
- **Commits.** Conventional Commits (`feat:`, `fix:`, `chore:`), small and scoped.
|
|
211
209
|
|
|
212
210
|
## Commands
|
|
@@ -239,4 +237,3 @@ uv run pytest -q # tests (no GPU; model code is import-
|
|
|
239
237
|
as `{error:{code,message}}` with the right status. Update the API list in `README.md`.
|
|
240
238
|
- **New port/handle type** → `PortKind` in `graph/schema.py` (+ `port_satisfies` if it has coercions).
|
|
241
239
|
- **New device/memory behaviour** → behind `DevicePolicy` in `device/`; never in a component.
|
|
242
|
-
- **New ComfyUI import behaviour** → `importer/comfy.py` only.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: inline-core
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.3
|
|
4
4
|
Summary: The generation engine behind Inline Studio.
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Requires-Dist: numpy>=1.26
|
|
@@ -53,7 +53,7 @@ already supports, so the multi-GPU split works on it from the start).
|
|
|
53
53
|
> Status: early, and running end to end against a stub engine. In place and tested: the graph engine,
|
|
54
54
|
> the typed `/v1` HTTP + websocket API (durable runs, streamed progress, coalescing), the model-dir
|
|
55
55
|
> scan, the device + memory policy (profiles, dtype, offload, int8), the low-level primitive node
|
|
56
|
-
> vocabulary
|
|
56
|
+
> vocabulary. The Z-Image loader is written and validates on a GPU.
|
|
57
57
|
> Cross-request batching, single-image multi-GPU (an xDiT worker group behind the sampler seam, with
|
|
58
58
|
> the policy and IPC round-trip tested), and out-of-process custom nodes are built as seams but not
|
|
59
59
|
> yet running on real hardware.
|
|
@@ -131,8 +131,7 @@ Underneath, a **low-level primitive vocabulary** exists (`load/diffusion-model`,
|
|
|
131
131
|
`load/text-encoder`, `encode/text`, `latent/empty`, `sample`, `vae/decode`, `vae/encode`) - the
|
|
132
132
|
ComfyUI-equivalent decomposed graph, kept for validation/execution - but these are marked **`hidden`**
|
|
133
133
|
and never appear in the add-node menu. Engine handles (`model`, `vae`, `text-encoder`, `conditioning`,
|
|
134
|
-
`latent`) are typed sockets between nodes; only media outputs become Frames with take history.
|
|
135
|
-
best-effort ComfyUI importer maps existing workflows onto the primitives.
|
|
134
|
+
`latent`) are typed sockets between nodes; only media outputs become Frames with take history.
|
|
136
135
|
|
|
137
136
|
## Multi-GPU: split one image across GPUs
|
|
138
137
|
|
|
@@ -11,7 +11,7 @@ already supports, so the multi-GPU split works on it from the start).
|
|
|
11
11
|
> Status: early, and running end to end against a stub engine. In place and tested: the graph engine,
|
|
12
12
|
> the typed `/v1` HTTP + websocket API (durable runs, streamed progress, coalescing), the model-dir
|
|
13
13
|
> scan, the device + memory policy (profiles, dtype, offload, int8), the low-level primitive node
|
|
14
|
-
> vocabulary
|
|
14
|
+
> vocabulary. The Z-Image loader is written and validates on a GPU.
|
|
15
15
|
> Cross-request batching, single-image multi-GPU (an xDiT worker group behind the sampler seam, with
|
|
16
16
|
> the policy and IPC round-trip tested), and out-of-process custom nodes are built as seams but not
|
|
17
17
|
> yet running on real hardware.
|
|
@@ -89,8 +89,7 @@ Underneath, a **low-level primitive vocabulary** exists (`load/diffusion-model`,
|
|
|
89
89
|
`load/text-encoder`, `encode/text`, `latent/empty`, `sample`, `vae/decode`, `vae/encode`) - the
|
|
90
90
|
ComfyUI-equivalent decomposed graph, kept for validation/execution - but these are marked **`hidden`**
|
|
91
91
|
and never appear in the add-node menu. Engine handles (`model`, `vae`, `text-encoder`, `conditioning`,
|
|
92
|
-
`latent`) are typed sockets between nodes; only media outputs become Frames with take history.
|
|
93
|
-
best-effort ComfyUI importer maps existing workflows onto the primitives.
|
|
92
|
+
`latent`) are typed sockets between nodes; only media outputs become Frames with take history.
|
|
94
93
|
|
|
95
94
|
## Multi-GPU: split one image across GPUs
|
|
96
95
|
|
|
@@ -49,12 +49,18 @@ def remote_sha(url: str, ref: str) -> str | None:
|
|
|
49
49
|
an update check must never fail the dialog."""
|
|
50
50
|
try:
|
|
51
51
|
_validate(url, ref)
|
|
52
|
-
|
|
52
|
+
# Also request the peeled ref: an exact-match pattern alone won't return the `^{}` line.
|
|
53
|
+
done = _git("ls-remote", url, ref, f"{ref}^{{}}", check=False)
|
|
53
54
|
except (FetchError, OSError, subprocess.SubprocessError):
|
|
54
55
|
return None
|
|
55
56
|
if done.returncode != 0:
|
|
56
57
|
return None
|
|
57
|
-
|
|
58
|
+
lines = [ln for ln in done.stdout.splitlines() if ln.strip()]
|
|
59
|
+
# An annotated tag lists its own object sha first and the commit it points to on a `^{}` line.
|
|
60
|
+
# Prefer the peeled commit, since the installed sha is a commit (`_resolve` uses `^{commit}`) -
|
|
61
|
+
# otherwise every annotated-tag release reads as perpetually "update available".
|
|
62
|
+
peeled = next((ln for ln in lines if ln.rstrip().endswith("^{}")), None)
|
|
63
|
+
line = peeled or (lines[0] if lines else "")
|
|
58
64
|
sha = line.split()[0] if line else ""
|
|
59
65
|
return sha or None
|
|
60
66
|
|
|
@@ -10,6 +10,12 @@ import os as _os
|
|
|
10
10
|
# `python -m inline_core.server`. A user-set value always wins.
|
|
11
11
|
_os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
|
|
12
12
|
|
|
13
|
+
# XET download acceleration tracks its bytes internally, so a model download reports no per-chunk
|
|
14
|
+
# progress - the popup's bar would sit at 0% then snap to done. The plain HTTP path streams a real
|
|
15
|
+
# fraction (and still resumes). huggingface_hub reads this at import, so it must be set before then.
|
|
16
|
+
# A user-set value always wins.
|
|
17
|
+
_os.environ.setdefault("HF_HUB_DISABLE_XET", "1")
|
|
18
|
+
|
|
13
19
|
import uvicorn
|
|
14
20
|
|
|
15
21
|
from ..config import data_dir, server_host, server_port
|
|
@@ -60,7 +66,6 @@ def main() -> None:
|
|
|
60
66
|
store = StudioStore(
|
|
61
67
|
studio_config.data_dir(),
|
|
62
68
|
studio_config.workspace_dir(),
|
|
63
|
-
default_comfy_url=studio_config.DEFAULT_COMFY_URL,
|
|
64
69
|
default_core_url=studio_config.DEFAULT_CORE_URL,
|
|
65
70
|
)
|
|
66
71
|
print(f"Studio data: {studio_config.data_dir()} | workspace: {studio_config.workspace_dir()}")
|
|
@@ -43,8 +43,8 @@ def _parse_params(raw: str | None) -> dict[str, Any]:
|
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
def _row_to_frame(row: sqlite3.Row) -> dict[str, Any]:
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
# A legacy 'comfy' row (ComfyUI removed) passes through unchanged; the UI renders it as a plain
|
|
47
|
+
# viewer. NULL/empty defaults to 'unset'.
|
|
48
48
|
return {
|
|
49
49
|
"id": row["id"],
|
|
50
50
|
"sequenceId": row["sequence_id"],
|
|
@@ -53,12 +53,10 @@ def _row_to_frame(row: sqlite3.Row) -> dict[str, Any]:
|
|
|
53
53
|
"position": row["position"],
|
|
54
54
|
"inputAssetId": row["input_asset_id"],
|
|
55
55
|
"heroTakeId": row["hero_take_id"],
|
|
56
|
-
"provider": provider,
|
|
56
|
+
"provider": row["provider"] or "unset",
|
|
57
57
|
"modelId": row["model_id"],
|
|
58
58
|
"params": _parse_params(row["params"]),
|
|
59
59
|
"workflowTemplateId": row["workflow_template_id"],
|
|
60
|
-
"comfyWorkflowName": row["comfy_workflow_name"],
|
|
61
|
-
"comfyWorkflowReady": row["comfy_workflow_ready"] == 1,
|
|
62
60
|
"createdAt": row["created_at"],
|
|
63
61
|
"updatedAt": row["updated_at"],
|
|
64
62
|
}
|
|
@@ -231,23 +229,12 @@ def set_provider(
|
|
|
231
229
|
)
|
|
232
230
|
else:
|
|
233
231
|
conn.execute(
|
|
234
|
-
"UPDATE frames SET provider = '
|
|
232
|
+
"UPDATE frames SET provider = 'unset', model_id = NULL, updated_at = ? WHERE id = ?",
|
|
235
233
|
(now, frame_id),
|
|
236
234
|
)
|
|
237
235
|
return get_frame(conn, frame_id)
|
|
238
236
|
|
|
239
237
|
|
|
240
|
-
def unlink_workflow(conn: sqlite3.Connection, frame_id: str) -> dict[str, Any]:
|
|
241
|
-
"""Detach a frame's ComfyUI workflow link (desktop legacy; a reset on the web path)."""
|
|
242
|
-
get_frame(conn, frame_id)
|
|
243
|
-
conn.execute(
|
|
244
|
-
"UPDATE frames SET comfy_workflow_name = NULL, comfy_workflow_ready = 0, "
|
|
245
|
-
"updated_at = ? WHERE id = ?",
|
|
246
|
-
(_now(), frame_id),
|
|
247
|
-
)
|
|
248
|
-
return get_frame(conn, frame_id)
|
|
249
|
-
|
|
250
|
-
|
|
251
238
|
def set_fal_params(
|
|
252
239
|
conn: sqlite3.Connection, frame_id: str, params: dict[str, Any]
|
|
253
240
|
) -> dict[str, Any]:
|
|
@@ -26,8 +26,8 @@ from . import moodboard as mb
|
|
|
26
26
|
def _source_output_port(source: dict[str, Any] | None, source_handle: str | None) -> str:
|
|
27
27
|
if source and source["type"] == "prompt":
|
|
28
28
|
return "text"
|
|
29
|
-
# An asset
|
|
30
|
-
if source and source["type"] in ("asset", "frame"):
|
|
29
|
+
# An asset, a rendered frame, or a Load Assets loader all become an ``input/image`` source node.
|
|
30
|
+
if source and source["type"] in ("asset", "frame", "loader"):
|
|
31
31
|
return "image"
|
|
32
32
|
return source_handle or "out" # a 'core' item's handles already are Core port ids
|
|
33
33
|
|
|
@@ -75,6 +75,17 @@ def _item_to_node(
|
|
|
75
75
|
"type": "input/image",
|
|
76
76
|
"params": {"asset": {"ref": "path", "path": path}},
|
|
77
77
|
}
|
|
78
|
+
# A Load Assets loader feeds its hero (first) asset as a frozen image source.
|
|
79
|
+
if item["type"] == "loader":
|
|
80
|
+
asset_ids = data.get("assetIds") or []
|
|
81
|
+
path = resolve_asset_path(asset_ids[0]) if asset_ids else None
|
|
82
|
+
if not path:
|
|
83
|
+
return None
|
|
84
|
+
return {
|
|
85
|
+
"id": item["id"],
|
|
86
|
+
"type": "input/image",
|
|
87
|
+
"params": {"asset": {"ref": "path", "path": path}},
|
|
88
|
+
}
|
|
78
89
|
# A rendered frame wired into a Core node feeds its output image as a frozen source (its hero
|
|
79
90
|
# take), so nothing upstream of the frame is recomputed.
|
|
80
91
|
if item["type"] == "frame" and item.get("frameId"):
|
|
@@ -4,8 +4,7 @@ backend is Core (Python), not the legacy Node server.
|
|
|
4
4
|
|
|
5
5
|
Channel args arrive as a positional list (the ``{channel, args}`` wire shape). Each handler unpacks
|
|
6
6
|
them and returns the value to wrap in Ok. Not-yet-ported surfaces (generation, timeline,
|
|
7
|
-
export
|
|
8
|
-
"no handler".
|
|
7
|
+
export) register clear stubs so the UI degrades gracefully instead of a "no handler".
|
|
9
8
|
"""
|
|
10
9
|
|
|
11
10
|
from __future__ import annotations
|
|
@@ -73,7 +72,6 @@ def register_studio_handlers(
|
|
|
73
72
|
reg("dialog:pickDirectory", lambda *_: str(cfg.workspace_dir()))
|
|
74
73
|
reg("app:version", lambda: app_version)
|
|
75
74
|
reg("settings:get", store.get_settings)
|
|
76
|
-
reg("settings:setComfyUrl", store.set_comfy_url)
|
|
77
75
|
reg("settings:setCoreUrl", store.set_core_url)
|
|
78
76
|
reg("core:status", core_status)
|
|
79
77
|
reg("core:models", core_models)
|
|
@@ -117,7 +115,6 @@ def register_studio_handlers(
|
|
|
117
115
|
reg("frames:rename", lambda fid, name: fr.rename_frame(conn(), fid, name))
|
|
118
116
|
reg("frames:reorder", lambda ids: fr.reorder_frames(conn(), ids))
|
|
119
117
|
reg("frames:clone", lambda fid: fr.clone_frame(conn(), fid))
|
|
120
|
-
reg("frames:unlink", lambda fid: fr.unlink_workflow(conn(), fid))
|
|
121
118
|
reg("frames:setHero", lambda fid, take_id: fr.set_hero(conn(), fid, take_id))
|
|
122
119
|
reg("frames:listTakes", lambda fid: fr.list_takes(conn(), fid))
|
|
123
120
|
reg("frames:heroTakes", lambda: fr.hero_takes(conn()))
|
|
@@ -168,6 +165,7 @@ def register_studio_handlers(
|
|
|
168
165
|
reg("moodboard:addLayer", lambda x, y: mb.add_layer(conn(), x, y))
|
|
169
166
|
reg("moodboard:addDirector", lambda x, y: mb.add_director(conn(), x, y))
|
|
170
167
|
reg("moodboard:addTrim", lambda x, y: mb.add_trim(conn(), x, y))
|
|
168
|
+
reg("moodboard:addLoader", lambda x, y: mb.add_loader(conn(), x, y))
|
|
171
169
|
reg("moodboard:addPrompt", lambda x, y: mb.add_prompt(conn(), x, y))
|
|
172
170
|
reg("moodboard:addCoreNode", lambda t, x, y: mb.add_core_node(conn(), t, x, y))
|
|
173
171
|
reg(
|
|
@@ -213,10 +211,6 @@ def register_studio_handlers(
|
|
|
213
211
|
reg("falSettings:status", store.fal_status)
|
|
214
212
|
reg("falSettings:setApiKey", store.set_fal_key)
|
|
215
213
|
reg("falSettings:clearApiKey", store.clear_fal_key)
|
|
216
|
-
reg("comfy:status", lambda: {"reachable": False, "url": ""})
|
|
217
|
-
for ch in ("linkFrame", "uploadInputs", "pullWorkflow", "saveLiveWorkflow", "pushWorkflow",
|
|
218
|
-
"pullLatest", "latestRun", "captureOutput"):
|
|
219
|
-
reg(f"comfy:{ch}", not_wired("Embedded ComfyUI"))
|
|
220
214
|
# --- timeline (director/trim/export via ffmpeg) + folder export -----------------------------
|
|
221
215
|
if timeline is not None:
|
|
222
216
|
from .timeline.render import export_frames
|
|
@@ -16,7 +16,6 @@ from __future__ import annotations
|
|
|
16
16
|
|
|
17
17
|
import asyncio
|
|
18
18
|
import shutil
|
|
19
|
-
import threading
|
|
20
19
|
from collections.abc import Callable
|
|
21
20
|
from pathlib import Path
|
|
22
21
|
from typing import Any
|
|
@@ -138,45 +137,40 @@ class ModelDownloads:
|
|
|
138
137
|
) -> None:
|
|
139
138
|
"""Fetch a component's single file from its repo into ``models/<category>/`` (flat, so the
|
|
140
139
|
node's dropdown lists it). Downloads into a ``.part`` staging dir first, then moves the file
|
|
141
|
-
into place under its basename, so a half-finished download never looks installed.
|
|
142
|
-
|
|
140
|
+
into place under its basename, so a half-finished download never looks installed.
|
|
141
|
+
|
|
142
|
+
Progress comes from huggingface_hub's own download counter via ``tqdm_class`` - real
|
|
143
|
+
per-chunk motion, and it still resumes a partial file. (XET is disabled at process start so
|
|
144
|
+
the plain HTTP path is used; XET reports nothing to tqdm.)"""
|
|
145
|
+
from huggingface_hub import hf_hub_download
|
|
146
|
+
from huggingface_hub.utils import HfHubHTTPError
|
|
143
147
|
|
|
144
148
|
category_dir: Path = provider.download_target(comp)
|
|
145
149
|
staging = category_dir / (comp.filename + ".part")
|
|
146
|
-
shutil.rmtree(staging, ignore_errors=True)
|
|
147
|
-
|
|
148
|
-
files = _wanted_files(HfApi(), comp)
|
|
149
|
-
total = sum(size for _, size in files)
|
|
150
150
|
on_progress(0.0, f"Downloading {comp.label}…")
|
|
151
|
+
tqdm_class = _progress_tqdm(on_progress, comp.label)
|
|
152
|
+
|
|
153
|
+
def _fetch(token: bool | None) -> str:
|
|
154
|
+
return hf_hub_download(
|
|
155
|
+
comp.repo,
|
|
156
|
+
comp.repo_file,
|
|
157
|
+
local_dir=str(staging),
|
|
158
|
+
token=token,
|
|
159
|
+
tqdm_class=tqdm_class,
|
|
160
|
+
)
|
|
151
161
|
|
|
152
|
-
# ``hf_hub_download`` blocks for the whole (multi-GB) file with no per-byte callback, so a
|
|
153
|
-
# naive "emit after each file" jumps 0 → 99%. Instead, a watcher thread polls the staging
|
|
154
|
-
# tree's byte size while the download runs and streams a real fraction. It only reports a
|
|
155
|
-
# fraction when the repo metadata gave us a real total; otherwise size is unknown and we
|
|
156
|
-
# leave the 0 → 1 endpoints (the status text still shows activity).
|
|
157
|
-
stop = threading.Event()
|
|
158
|
-
|
|
159
|
-
def _watch() -> None:
|
|
160
|
-
while not stop.wait(0.5):
|
|
161
|
-
if total <= 0:
|
|
162
|
-
continue
|
|
163
|
-
got = _dir_size(staging)
|
|
164
|
-
if got > 0:
|
|
165
|
-
on_progress(min(0.98, got / total), f"Downloading {comp.label}…")
|
|
166
|
-
|
|
167
|
-
watcher = threading.Thread(target=_watch, name="model-download-progress", daemon=True)
|
|
168
|
-
watcher.start()
|
|
169
162
|
try:
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
163
|
+
path = _fetch(None) # ambient token, e.g. for a gated repo the user has access to
|
|
164
|
+
except HfHubHTTPError as error:
|
|
165
|
+
# A stale/invalid cached HF token 401s even on a public repo (HF masks it as "not
|
|
166
|
+
# found"). Retry anonymously so a bad token never blocks a public model download.
|
|
167
|
+
if getattr(getattr(error, "response", None), "status_code", None) not in (401, 403):
|
|
168
|
+
raise
|
|
169
|
+
shutil.rmtree(staging, ignore_errors=True)
|
|
170
|
+
path = _fetch(False)
|
|
175
171
|
|
|
176
172
|
category_dir.mkdir(parents=True, exist_ok=True)
|
|
177
|
-
|
|
178
|
-
dest = category_dir / Path(rfilename).name
|
|
179
|
-
shutil.move(str(staging / rfilename), str(dest))
|
|
173
|
+
shutil.move(path, str(category_dir / comp.filename))
|
|
180
174
|
shutil.rmtree(staging, ignore_errors=True)
|
|
181
175
|
on_progress(1.0, f"{comp.label} ready")
|
|
182
176
|
|
|
@@ -210,30 +204,19 @@ def _source_label(component: Any) -> str:
|
|
|
210
204
|
return component.repo
|
|
211
205
|
|
|
212
206
|
|
|
213
|
-
def
|
|
214
|
-
"""
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
def _wanted_files(api: Any, comp: Any) -> list[tuple[str, int]]:
|
|
231
|
-
"""(rfilename, size) for the single file this component pulls, sized from the repo metadata.
|
|
232
|
-
|
|
233
|
-
Falls back to size 0 (progress by count) if the file isn't in the listing - the download still
|
|
234
|
-
proceeds; ``hf_hub_download`` is the source of truth for whether the path exists."""
|
|
235
|
-
info = api.model_info(comp.repo, files_metadata=True)
|
|
236
|
-
for sibling in info.siblings:
|
|
237
|
-
if sibling.rfilename == comp.repo_file:
|
|
238
|
-
return [(comp.repo_file, int(sibling.size or 0))]
|
|
239
|
-
return [(comp.repo_file, 0)]
|
|
207
|
+
def _progress_tqdm(on_progress: Callable[[float, str], None], label: str) -> Any:
|
|
208
|
+
"""A tqdm subclass handed to ``hf_hub_download`` as ``tqdm_class``; it forwards the download
|
|
209
|
+
counter to ``on_progress``. It sums ``n`` itself because ``tqdm.update`` short-circuits (never
|
|
210
|
+
touching ``self.n``) when its bar is disabled - which it is on a headless server. The 0.99 cap
|
|
211
|
+
leaves the final 1.0 for the post-move ``ready``."""
|
|
212
|
+
from tqdm.auto import tqdm
|
|
213
|
+
|
|
214
|
+
class _Tqdm(tqdm):
|
|
215
|
+
def update(self, n: float | None = 1) -> Any:
|
|
216
|
+
done = super().update(n)
|
|
217
|
+
self._forwarded = getattr(self, "_forwarded", 0.0) + (n or 0.0)
|
|
218
|
+
if self.total:
|
|
219
|
+
on_progress(min(0.99, self._forwarded / self.total), f"Downloading {label}…")
|
|
220
|
+
return done
|
|
221
|
+
|
|
222
|
+
return _Tqdm
|
|
@@ -233,6 +233,14 @@ def add_trim(conn: sqlite3.Connection, x: float, y: float) -> dict[str, Any]:
|
|
|
233
233
|
)
|
|
234
234
|
|
|
235
235
|
|
|
236
|
+
def add_loader(conn: sqlite3.Connection, x: float, y: float) -> dict[str, Any]:
|
|
237
|
+
# A "Load Assets" node holds library asset refs in its data (no frame, no frame_inputs) and
|
|
238
|
+
# feeds its hero (first) asset downstream via graph_build.
|
|
239
|
+
return _insert_item(
|
|
240
|
+
conn, item_type="loader", x=x, y=y, width=220, height=200, data={"assetIds": []}
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
236
244
|
def add_prompt(conn: sqlite3.Connection, x: float, y: float) -> dict[str, Any]:
|
|
237
245
|
return _insert_item(
|
|
238
246
|
conn, item_type="prompt", x=x, y=y, width=240, height=120, data={"promptText": ""}
|
|
@@ -53,14 +53,12 @@ class StudioStore:
|
|
|
53
53
|
app_data_dir: str | Path,
|
|
54
54
|
workspace_dir: str | Path,
|
|
55
55
|
*,
|
|
56
|
-
default_comfy_url: str = "http://127.0.0.1:8188",
|
|
57
56
|
default_core_url: str = "http://127.0.0.1:8848",
|
|
58
57
|
) -> None:
|
|
59
58
|
self._app_data = Path(app_data_dir)
|
|
60
59
|
self._workspace = Path(workspace_dir)
|
|
61
60
|
self._app_data.mkdir(parents=True, exist_ok=True)
|
|
62
61
|
self._workspace.mkdir(parents=True, exist_ok=True)
|
|
63
|
-
self._default_comfy_url = default_comfy_url
|
|
64
62
|
self._default_core_url = default_core_url
|
|
65
63
|
self._conn: sqlite3.Connection | None = None
|
|
66
64
|
self._folder: Path | None = None
|
|
@@ -237,7 +235,6 @@ class StudioStore:
|
|
|
237
235
|
def get_settings(self) -> dict[str, str]:
|
|
238
236
|
saved = self._read_settings()
|
|
239
237
|
return {
|
|
240
|
-
"comfyUrl": self._non_empty(saved.get("comfyUrl")) or self._default_comfy_url,
|
|
241
238
|
"coreUrl": self._non_empty(saved.get("coreUrl")) or self._default_core_url,
|
|
242
239
|
}
|
|
243
240
|
|
|
@@ -245,11 +242,6 @@ class StudioStore:
|
|
|
245
242
|
self._settings_file().write_text(json.dumps(settings, indent=2), encoding="utf-8")
|
|
246
243
|
return settings
|
|
247
244
|
|
|
248
|
-
def set_comfy_url(self, url: str) -> dict[str, str]:
|
|
249
|
-
settings = self.get_settings()
|
|
250
|
-
settings["comfyUrl"] = url.strip() or self._default_comfy_url
|
|
251
|
-
return self._save_settings(settings)
|
|
252
|
-
|
|
253
245
|
def set_core_url(self, url: str) -> dict[str, str]:
|
|
254
246
|
settings = self.get_settings()
|
|
255
247
|
settings["coreUrl"] = url.strip() or self._default_core_url
|
|
@@ -388,6 +388,33 @@ def test_reinstall_recovers_from_a_corrupt_cached_mirror(
|
|
|
388
388
|
assert (mirror / "HEAD").is_file()
|
|
389
389
|
|
|
390
390
|
|
|
391
|
+
def test_remote_sha_peels_an_annotated_tag_to_its_commit(tmp_path: Path) -> None:
|
|
392
|
+
"""An annotated tag's ls-remote lists the tag object first and the commit on a `^{}` line. The
|
|
393
|
+
update check compares against the installed *commit*, so remote_sha must peel - otherwise every
|
|
394
|
+
annotated-tag release reads as perpetually 'update available'."""
|
|
395
|
+
from inline_core.extensions.fetch import remote_sha
|
|
396
|
+
|
|
397
|
+
repo = tmp_path / "annotated-repo"
|
|
398
|
+
repo.mkdir()
|
|
399
|
+
_git("init", "-q", "-b", "main", cwd=repo)
|
|
400
|
+
_git("config", "user.email", "test@example.com", cwd=repo)
|
|
401
|
+
_git("config", "user.name", "Test", cwd=repo)
|
|
402
|
+
(repo / "file.txt").write_text("hi", encoding="utf-8")
|
|
403
|
+
_git("add", "-A", cwd=repo)
|
|
404
|
+
_git("commit", "-q", "-m", "initial", cwd=repo)
|
|
405
|
+
_git("tag", "-a", "v1.0.0", "-m", "release", cwd=repo) # annotated, not lightweight
|
|
406
|
+
|
|
407
|
+
commit = subprocess.run(
|
|
408
|
+
["git", "rev-parse", "v1.0.0^{commit}"],
|
|
409
|
+
cwd=repo,
|
|
410
|
+
check=True,
|
|
411
|
+
capture_output=True,
|
|
412
|
+
text=True,
|
|
413
|
+
).stdout.strip()
|
|
414
|
+
|
|
415
|
+
assert remote_sha(f"file://{repo}", "v1.0.0") == commit
|
|
416
|
+
|
|
417
|
+
|
|
391
418
|
def test_switching_to_an_uninstalled_version_is_refused(
|
|
392
419
|
installer: Installer, tmp_path: Path
|
|
393
420
|
) -> None:
|
|
@@ -62,8 +62,9 @@ def test_set_model_and_provider(conn) -> None:
|
|
|
62
62
|
swapped = fr.set_model(conn, frame["id"], "m/2", "video", {"b": 2})
|
|
63
63
|
assert swapped["modelId"] == "m/2" and swapped["kind"] == "video"
|
|
64
64
|
assert swapped["params"] == {"b": 2}
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
# A non-fal set_provider resets the frame to 'unset' (the neutral, engine-less state).
|
|
66
|
+
reset = fr.set_provider(conn, frame["id"], "unset")
|
|
67
|
+
assert reset["provider"] == "unset" and reset["modelId"] is None
|
|
67
68
|
|
|
68
69
|
|
|
69
70
|
def test_rename_reorder_delete(conn) -> None:
|
|
@@ -35,7 +35,7 @@ def test_build_workflow_graph_prompt_into_zimage(tmp_path) -> None:
|
|
|
35
35
|
|
|
36
36
|
def test_build_workflow_graph_frame_output_into_zimage_image(tmp_path) -> None:
|
|
37
37
|
"""A rendered frame wired into Z-Image's image port becomes an input/image source node pointing
|
|
38
|
-
at the frame's hero take - no dangling edge to a non-emitted node
|
|
38
|
+
at the frame's hero take - no dangling edge to a non-emitted node."""
|
|
39
39
|
store = _store(tmp_path)
|
|
40
40
|
conn = store.conn()
|
|
41
41
|
z = mb.add_core_node(conn, "alibaba/z-image-turbo", 400, 200)
|
|
@@ -55,6 +55,30 @@ def test_build_workflow_graph_frame_output_into_zimage_image(tmp_path) -> None:
|
|
|
55
55
|
assert take["id"] # hero take exists
|
|
56
56
|
|
|
57
57
|
|
|
58
|
+
def test_build_workflow_graph_loader_into_zimage_image(tmp_path) -> None:
|
|
59
|
+
"""A standalone Load Assets loader (its hero asset) wired into Z-Image becomes an input/image
|
|
60
|
+
source node - no frame involved."""
|
|
61
|
+
store = _store(tmp_path)
|
|
62
|
+
conn = store.conn()
|
|
63
|
+
conn.execute(
|
|
64
|
+
"INSERT INTO assets (id, project_id, name, file_path, kind, created_at) "
|
|
65
|
+
"VALUES ('a1', ?, 'a', 'assets/a.png', 'image', 0)",
|
|
66
|
+
(mb._project_id(conn),),
|
|
67
|
+
)
|
|
68
|
+
z = mb.add_core_node(conn, "alibaba/z-image-turbo", 400, 200)
|
|
69
|
+
loader = mb.add_loader(conn, 80, 200)
|
|
70
|
+
assert loader["type"] == "loader" and loader["data"]["assetIds"] == []
|
|
71
|
+
mb.update_item(conn, loader["id"], {"data": {"assetIds": ["a1"]}})
|
|
72
|
+
mb.create_connector(conn, loader["id"], z["id"], "out", "image")
|
|
73
|
+
|
|
74
|
+
graph, _ = build_workflow_graph(conn, store.folder(), z["id"])
|
|
75
|
+
by_id = {n["id"]: n for n in graph["nodes"]}
|
|
76
|
+
loader_node = by_id[loader["id"]]
|
|
77
|
+
assert loader_node["type"] == "input/image"
|
|
78
|
+
assert loader_node["params"]["asset"]["path"] == str(store.folder() / "assets/a.png")
|
|
79
|
+
assert by_id[z["id"]]["inputs"]["image"] == {"from": loader["id"], "output": "image"}
|
|
80
|
+
|
|
81
|
+
|
|
58
82
|
class _Events:
|
|
59
83
|
def __init__(self) -> None:
|
|
60
84
|
self.sent: list[tuple[str, dict]] = []
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""The explicit model-download backend (studio/models.py) - the torch-free bits: which repo file a
|
|
2
|
+
component pulls and the requirements view for unknown node types. The Hugging Face download itself
|
|
3
|
+
is not exercised here (no network); the runner/requirements tests cover presence logic."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import types
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
from inline_core.studio.models import (
|
|
13
|
+
ModelDownloads,
|
|
14
|
+
_component_json,
|
|
15
|
+
_progress_tqdm,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_progress_tqdm_forwards_the_download_fraction():
|
|
20
|
+
"""The tqdm subclass handed to hf_hub_download must turn its byte counter into a 0..0.99
|
|
21
|
+
fraction on our callback."""
|
|
22
|
+
pytest.importorskip("tqdm")
|
|
23
|
+
seen: list[float] = []
|
|
24
|
+
cls = _progress_tqdm(lambda frac, status: seen.append(frac), "VAE")
|
|
25
|
+
bar = cls(total=100, disable=True)
|
|
26
|
+
bar.update(50)
|
|
27
|
+
bar.update(50) # reaches total, but the cap keeps 1.0 for the post-move "ready"
|
|
28
|
+
assert seen == [0.5, 0.99]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_requirements_empty_for_unknown_node_type():
|
|
32
|
+
downloads = ModelDownloads(events=None)
|
|
33
|
+
assert downloads.requirements("no/such-node") == {
|
|
34
|
+
"components": [],
|
|
35
|
+
"allPresent": True,
|
|
36
|
+
"estimate": None, # no requirements + no policy -> no fit estimate
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_download_retries_anonymously_when_a_cached_token_is_invalid(tmp_path, monkeypatch):
|
|
41
|
+
"""A stale/invalid cached HF token 401s even on a public repo (masked as "not found"). The
|
|
42
|
+
download must drop the token and retry anonymously, not fail."""
|
|
43
|
+
hub = pytest.importorskip("huggingface_hub")
|
|
44
|
+
from huggingface_hub.utils import HfHubHTTPError
|
|
45
|
+
|
|
46
|
+
tokens_seen: list[object] = []
|
|
47
|
+
|
|
48
|
+
def fake_download(repo, rfilename, local_dir, token=None, tqdm_class=None):
|
|
49
|
+
tokens_seen.append(token)
|
|
50
|
+
if token is not False: # ambient (stale) token -> 401 the way HF does
|
|
51
|
+
response = types.SimpleNamespace(status_code=401, headers={}, request=None)
|
|
52
|
+
raise HfHubHTTPError("401 Client Error. Repository Not Found", response=response)
|
|
53
|
+
dest = Path(local_dir) / rfilename
|
|
54
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
dest.write_bytes(b"data")
|
|
56
|
+
return str(dest)
|
|
57
|
+
|
|
58
|
+
monkeypatch.setattr(hub, "hf_hub_download", fake_download)
|
|
59
|
+
|
|
60
|
+
downloads = ModelDownloads(events=None)
|
|
61
|
+
comp = types.SimpleNamespace(repo="lokCX/4x-Ultrasharp", repo_file="w.pth", filename="w.pth",
|
|
62
|
+
label="4x UltraSharp")
|
|
63
|
+
provider = types.SimpleNamespace(download_target=lambda c: tmp_path)
|
|
64
|
+
downloads._download_component(provider, comp, lambda frac, status: None)
|
|
65
|
+
|
|
66
|
+
assert (tmp_path / "w.pth").read_bytes() == b"data"
|
|
67
|
+
assert tokens_seen == [None, False] # tried the ambient token, then fell back to anonymous
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_component_json_shape():
|
|
71
|
+
comp = types.SimpleNamespace(
|
|
72
|
+
id="vae",
|
|
73
|
+
label="VAE",
|
|
74
|
+
category="vae",
|
|
75
|
+
present=False,
|
|
76
|
+
local_path="vae/ae.safetensors",
|
|
77
|
+
repo="Comfy-Org/z_image",
|
|
78
|
+
filename="ae.safetensors",
|
|
79
|
+
repo_file="split_files/vae/ae.safetensors",
|
|
80
|
+
)
|
|
81
|
+
assert _component_json(comp) == {
|
|
82
|
+
"id": "vae",
|
|
83
|
+
"label": "VAE",
|
|
84
|
+
"category": "vae",
|
|
85
|
+
"present": False,
|
|
86
|
+
"localPath": "vae/ae.safetensors",
|
|
87
|
+
"repo": "Comfy-Org/z_image",
|
|
88
|
+
# "which model" is the repo + the exact file this component pulls.
|
|
89
|
+
"source": "Comfy-Org/z_image/ae.safetensors",
|
|
90
|
+
}
|
|
@@ -97,9 +97,6 @@ def test_asset_upload_and_media_serving(client) -> None:
|
|
|
97
97
|
|
|
98
98
|
def test_unported_channels_degrade_gracefully(client) -> None:
|
|
99
99
|
rpc(client, "project:create", {"name": "P", "parentDir": None})
|
|
100
|
-
# Embedded ComfyUI is desktop-only - a clear error, not a crash.
|
|
101
|
-
cf = rpc(client, "comfy:linkFrame", "frame-x")
|
|
102
|
-
assert cf["ok"] is False and "comfyui" in cf["error"].lower()
|
|
103
100
|
# Cancel is a safe no-op.
|
|
104
101
|
assert rpc(client, "generation:cancel")["ok"] is True
|
|
105
102
|
|