inline-core 1.1.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 (115) hide show
  1. inline_core-1.1.1/.gitignore +20 -0
  2. inline_core-1.1.1/.python-version +1 -0
  3. inline_core-1.1.1/CLAUDE.md +212 -0
  4. inline_core-1.1.1/PKG-INFO +35 -0
  5. inline_core-1.1.1/README.md +177 -0
  6. inline_core-1.1.1/main.py +93 -0
  7. inline_core-1.1.1/pyproject.toml +82 -0
  8. inline_core-1.1.1/scripts/reference.py +65 -0
  9. inline_core-1.1.1/src/inline_core/__init__.py +7 -0
  10. inline_core-1.1.1/src/inline_core/components/__init__.py +1 -0
  11. inline_core-1.1.1/src/inline_core/components/conditioning.py +20 -0
  12. inline_core-1.1.1/src/inline_core/components/interfaces.py +91 -0
  13. inline_core-1.1.1/src/inline_core/config.py +33 -0
  14. inline_core-1.1.1/src/inline_core/device/__init__.py +1 -0
  15. inline_core-1.1.1/src/inline_core/device/auto.py +35 -0
  16. inline_core-1.1.1/src/inline_core/device/detect.py +41 -0
  17. inline_core-1.1.1/src/inline_core/device/memory.py +168 -0
  18. inline_core-1.1.1/src/inline_core/device/policy.py +82 -0
  19. inline_core-1.1.1/src/inline_core/device/types.py +31 -0
  20. inline_core-1.1.1/src/inline_core/errors.py +42 -0
  21. inline_core-1.1.1/src/inline_core/graph/__init__.py +1 -0
  22. inline_core-1.1.1/src/inline_core/graph/cache.py +83 -0
  23. inline_core-1.1.1/src/inline_core/graph/descriptor.py +81 -0
  24. inline_core-1.1.1/src/inline_core/graph/executor.py +102 -0
  25. inline_core-1.1.1/src/inline_core/graph/primitives.py +137 -0
  26. inline_core-1.1.1/src/inline_core/graph/registry.py +61 -0
  27. inline_core-1.1.1/src/inline_core/graph/runners.py +72 -0
  28. inline_core-1.1.1/src/inline_core/graph/schema.py +136 -0
  29. inline_core-1.1.1/src/inline_core/graph/topo.py +49 -0
  30. inline_core-1.1.1/src/inline_core/graph/validate.py +56 -0
  31. inline_core-1.1.1/src/inline_core/importer/__init__.py +1 -0
  32. inline_core-1.1.1/src/inline_core/importer/comfy.py +162 -0
  33. inline_core-1.1.1/src/inline_core/media.py +11 -0
  34. inline_core-1.1.1/src/inline_core/models/__init__.py +8 -0
  35. inline_core-1.1.1/src/inline_core/models/catalog.py +98 -0
  36. inline_core-1.1.1/src/inline_core/models/zimage/__init__.py +11 -0
  37. inline_core-1.1.1/src/inline_core/models/zimage/requirements.py +180 -0
  38. inline_core-1.1.1/src/inline_core/models/zimage/runner.py +386 -0
  39. inline_core-1.1.1/src/inline_core/parallel/__init__.py +13 -0
  40. inline_core-1.1.1/src/inline_core/parallel/config.py +50 -0
  41. inline_core-1.1.1/src/inline_core/parallel/group.py +136 -0
  42. inline_core-1.1.1/src/inline_core/parallel/launch.py +44 -0
  43. inline_core-1.1.1/src/inline_core/parallel/protocol.py +53 -0
  44. inline_core-1.1.1/src/inline_core/parallel/registry.py +31 -0
  45. inline_core-1.1.1/src/inline_core/parallel/worker.py +75 -0
  46. inline_core-1.1.1/src/inline_core/runtime/__init__.py +1 -0
  47. inline_core-1.1.1/src/inline_core/runtime/context.py +31 -0
  48. inline_core-1.1.1/src/inline_core/runtime/file_store.py +51 -0
  49. inline_core-1.1.1/src/inline_core/runtime/progress.py +83 -0
  50. inline_core-1.1.1/src/inline_core/runtime/run.py +113 -0
  51. inline_core-1.1.1/src/inline_core/runtime/store.py +18 -0
  52. inline_core-1.1.1/src/inline_core/sampling/__init__.py +1 -0
  53. inline_core-1.1.1/src/inline_core/sampling/batch.py +116 -0
  54. inline_core-1.1.1/src/inline_core/server/__init__.py +1 -0
  55. inline_core-1.1.1/src/inline_core/server/__main__.py +61 -0
  56. inline_core-1.1.1/src/inline_core/server/app.py +327 -0
  57. inline_core-1.1.1/src/inline_core/server/assets.py +47 -0
  58. inline_core-1.1.1/src/inline_core/server/bootstrap.py +21 -0
  59. inline_core-1.1.1/src/inline_core/server/frontend.py +37 -0
  60. inline_core-1.1.1/src/inline_core/server/manager.py +195 -0
  61. inline_core-1.1.1/src/inline_core/server/rpc.py +60 -0
  62. inline_core-1.1.1/src/inline_core/server/run_store.py +155 -0
  63. inline_core-1.1.1/src/inline_core/server/serialize.py +144 -0
  64. inline_core-1.1.1/src/inline_core/studio/__init__.py +7 -0
  65. inline_core-1.1.1/src/inline_core/studio/assets.py +194 -0
  66. inline_core-1.1.1/src/inline_core/studio/config.py +29 -0
  67. inline_core-1.1.1/src/inline_core/studio/fal.py +237 -0
  68. inline_core-1.1.1/src/inline_core/studio/frames.py +552 -0
  69. inline_core-1.1.1/src/inline_core/studio/generation.py +136 -0
  70. inline_core-1.1.1/src/inline_core/studio/graph_build.py +109 -0
  71. inline_core-1.1.1/src/inline_core/studio/handlers.py +236 -0
  72. inline_core-1.1.1/src/inline_core/studio/models.py +173 -0
  73. inline_core-1.1.1/src/inline_core/studio/moodboard.py +400 -0
  74. inline_core-1.1.1/src/inline_core/studio/schema.py +278 -0
  75. inline_core-1.1.1/src/inline_core/studio/store.py +291 -0
  76. inline_core-1.1.1/src/inline_core/studio/timeline/__init__.py +6 -0
  77. inline_core-1.1.1/src/inline_core/studio/timeline/compose.py +130 -0
  78. inline_core-1.1.1/src/inline_core/studio/timeline/ffmpeg.py +76 -0
  79. inline_core-1.1.1/src/inline_core/studio/timeline/render.py +120 -0
  80. inline_core-1.1.1/src/inline_core/studio/timeline/resolve.py +189 -0
  81. inline_core-1.1.1/src/inline_core/takes.py +31 -0
  82. inline_core-1.1.1/tests/helpers.py +79 -0
  83. inline_core-1.1.1/tests/test_cache.py +47 -0
  84. inline_core-1.1.1/tests/test_catalog.py +64 -0
  85. inline_core-1.1.1/tests/test_comfy_import.py +80 -0
  86. inline_core-1.1.1/tests/test_config.py +26 -0
  87. inline_core-1.1.1/tests/test_executor.py +48 -0
  88. inline_core-1.1.1/tests/test_file_store.py +32 -0
  89. inline_core-1.1.1/tests/test_frontend_serving.py +65 -0
  90. inline_core-1.1.1/tests/test_hidden_nodes.py +37 -0
  91. inline_core-1.1.1/tests/test_memory_policy.py +87 -0
  92. inline_core-1.1.1/tests/test_parallel_group.py +51 -0
  93. inline_core-1.1.1/tests/test_primitives.py +96 -0
  94. inline_core-1.1.1/tests/test_rpc_bridge.py +51 -0
  95. inline_core-1.1.1/tests/test_run_store.py +66 -0
  96. inline_core-1.1.1/tests/test_schema.py +51 -0
  97. inline_core-1.1.1/tests/test_server.py +121 -0
  98. inline_core-1.1.1/tests/test_studio_assets.py +72 -0
  99. inline_core-1.1.1/tests/test_studio_fal.py +63 -0
  100. inline_core-1.1.1/tests/test_studio_frames.py +125 -0
  101. inline_core-1.1.1/tests/test_studio_generation.py +106 -0
  102. inline_core-1.1.1/tests/test_studio_models.py +94 -0
  103. inline_core-1.1.1/tests/test_studio_moodboard.py +101 -0
  104. inline_core-1.1.1/tests/test_studio_rpc.py +100 -0
  105. inline_core-1.1.1/tests/test_studio_schema.py +97 -0
  106. inline_core-1.1.1/tests/test_studio_store.py +109 -0
  107. inline_core-1.1.1/tests/test_studio_timeline.py +119 -0
  108. inline_core-1.1.1/tests/test_take_bytes.py +55 -0
  109. inline_core-1.1.1/tests/test_topo.py +29 -0
  110. inline_core-1.1.1/tests/test_validate.py +62 -0
  111. inline_core-1.1.1/tests/test_xfuser_sampler.py +88 -0
  112. inline_core-1.1.1/tests/test_zimage_resolve.py +122 -0
  113. inline_core-1.1.1/tests/test_zimage_runner.py +161 -0
  114. inline_core-1.1.1/uv.lock +2106 -0
  115. inline_core-1.1.1/webui.sh +149 -0
@@ -0,0 +1,20 @@
1
+ # Model weights (gigabytes). The engine's models root — `models_dir()` defaults to ./models, and
2
+ # runners scan ./models/<category>/ (e.g. diffusion_models/). Leading slash anchors this to the repo
3
+ # root ONLY, so the source package src/inline_core/models/ (code) stays tracked.
4
+ /models/
5
+
6
+ # Engine working data: the run DB and generated takes (also grows to gigabytes).
7
+ /.inline/
8
+
9
+ # Studio app data + project workspace (when created under the repo) and local server logs.
10
+ /.inline-studio-server/
11
+ /InlineStudioProjects/
12
+ *.log
13
+
14
+ # Python venv + bytecode
15
+ .venv/
16
+ __pycache__/
17
+ *.py[cod]
18
+
19
+ # Local env overrides
20
+ .env
@@ -0,0 +1 @@
1
+ 3.11
@@ -0,0 +1,212 @@
1
+ # Inline Core — Engineering Guide
2
+
3
+ Inline Core is the **generation engine behind Inline** (the Storyline / Inline Studio UI client). It
4
+ takes a **typed node graph (JSON)** and returns immutable renders ("takes"), running image and video
5
+ models across macOS, Windows, and Linux — from CPU-only boxes and low-VRAM laptops up to multi-GPU
6
+ machines that split a single image's sampling across GPUs (via xDiT). **It is the render backend that
7
+ replaces ComfyUI for Inline.**
8
+
9
+ > The UI client lives in the separate **Inline Studio / Storyline** repo (`inline-studio`, an Electron
10
+ > app). It drives this engine over the `/v1` HTTP + websocket API. Inline Core is headless and knows
11
+ > nothing about the UI.
12
+
13
+ > Read this file before changing code. It defines the architecture and the non-negotiable rules.
14
+ > `README.md` is the user/product-facing version of the same story; this is the engineering contract.
15
+
16
+ ## Mental model (everything is organised around this)
17
+
18
+ ```
19
+ Graph (typed nodes + edges) → Run → Take[] (immutable renders)
20
+ ```
21
+
22
+ - **Graph** — a JSON DAG of typed nodes. Edges are type-checked (`model`, `vae`, `conditioning`,
23
+ `latent`, media) **before** the run, so a bad graph is rejected at submit (422), never mid-denoise.
24
+ - **Run** — one execution of a target node's upstream closure. Durable (survives a restart) and
25
+ pollable; progress streams over a websocket.
26
+ - **Take** — one immutable output. Regenerating adds a take; **nothing is overwritten** (this mirrors
27
+ Inline Studio's frame/take model — the take history is the core value Comfy lacks).
28
+ - **Node** — has a **descriptor** (the data half: ports, params, file pickers — served at
29
+ `/v1/models`) and a **runner** (the behavior half). A descriptor with no runner is served and
30
+ type-checked but cannot execute yet.
31
+
32
+ ### The two boundaries that matter most (why this isn't ComfyUI)
33
+
34
+ 1. **Graph orchestration is decoupled from GPU work.** The executor runs cheap orchestration inline
35
+ and never runs the denoise loop itself — a model runner submits a `SampleJob` through the
36
+ **batched-sampler seam** (`sampling/batch.py`). The graph is the unit of caching; the sampler is
37
+ the unit of batching; the multi-GPU split routes through that same seam.
38
+ 2. **The device policy is the single owner of placement.** No node or component ever picks a device,
39
+ dtype, or offload. They ask `DevicePolicy.placement(role)`. So the same graph runs on a 4090, a
40
+ 6 GB laptop, pure CPU, or split across several GPUs, without touching the graph.
41
+
42
+ If you find yourself hardcoding a device in a component, or running a denoise loop inside the
43
+ executor, stop — you're breaking one of the two boundaries the whole design exists to keep.
44
+
45
+ ## Architecture
46
+
47
+ Headless Python. A FastAPI `/v1` server over a run manager, a node registry, and a device policy.
48
+
49
+ ```
50
+ HTTP/WS → server/app.py → RunManager → Executor → Registry (descriptor + runner)
51
+
52
+ runner "lowers" to → components (TextEncoder/Denoiser/VAE/…)
53
+
54
+ SampleJob → BatchedSampler (inline | xDiT worker group)
55
+ ```
56
+
57
+ - **`server/`** — the `/v1` API. `app.py` (routes), `manager.py` (validate → queue → run on a worker
58
+ thread → fan out events), `run_store.py` (SQLite durability), `bootstrap.py` (best-effort model
59
+ registration), `serialize.py` (contract JSON), `assets.py` (content-addressed upload).
60
+ - **`graph/`** — the engine core. `schema.py` (typed `Graph`/`Node`/`Edge` + JSON parser),
61
+ `descriptor.py` (node data half), `runners.py` (node behavior half + source nodes), `registry.py`
62
+ (descriptors + runners), `validate.py` + `topo.py` (type-check + order), `executor.py` (lazy
63
+ closure execution, node cache), `primitives.py` (the low-level node vocabulary), `cache.py`.
64
+ - **`components/`** — the five device-agnostic component interfaces (`TextEncoder`, `Scheduler`,
65
+ `Denoiser`, `Sampler`, `VAE`) plus opaque `Conditioning`/`Latents`. Placement comes from the ctx.
66
+ - **`sampling/`** — `batch.py`: the graph/GPU boundary. `SampleJob` → `BatchedSampler`
67
+ (`InlineBatchedSampler` today; `XFuserBatchedSampler` routes a parallel placement to the worker
68
+ group). Keep this module torch-free and mockable.
69
+ - **`device/`** — the policy. `policy.py` (interface: `Placement`, `Profile`, quant, attention),
70
+ `memory.py` (`MemoryPolicy`), `detect.py` / `auto.py` (enumerate GPUs, NVLink vs PCIe), `types.py`.
71
+ - **`parallel/`** — the xDiT (xfuser) worker group: one process per GPU via `torchrun`, talking over
72
+ local IPC behind the sampler seam. `launch.py`, `worker.py`, `group.py`, `registry.py`, `config.py`,
73
+ `protocol.py`. The HTTP server, DB, and graph stay single-process; only the denoise distributes.
74
+ - **`models/`** — `catalog.py` (scans the models root, feeds `options_from` selects + the registry
75
+ version) and the **model-runner subpackages** (e.g. `zimage/`), imported best-effort by
76
+ `server/bootstrap.py` so a torch-less install still boots.
77
+ - **`runtime/`** — `context.py` (`ExecutionContext`, `CancelToken`), `run.py` (`RunState`),
78
+ `progress.py` (events + emitters), `store.py` / `file_store.py` (`TakeStore`: owns take bytes/hash/
79
+ uri).
80
+ - **`importer/`** — `comfy.py`: best-effort ComfyUI-workflow → Inline-Core-graph mapping. All ComfyUI
81
+ format knowledge lives here.
82
+ - **`config.py`** — all env config, small and explicit. **`takes.py`**, **`media.py`**, **`errors.py`**
83
+ — domain primitives (`Take`/`AssetRef`, `MediaKind`, the error hierarchy).
84
+
85
+ ### Node vocabularies (three, and their status)
86
+
87
+ - **Source nodes** (`graph/runners.py`) — `input/text`, `input/image`. Runners exist; pure, no takes.
88
+ These are the closure boundary: the UI feeds curated inputs in as source nodes so nothing upstream
89
+ is recomputed.
90
+ - **Low-level primitives** (`graph/primitives.py`) — `load/diffusion-model`, `load/vae`,
91
+ `load/text-encoder`, `encode/text`, `latent/empty`, `sample`, `vae/decode`, `vae/encode`. These are
92
+ the ComfyUI-equivalent decomposed graph and the intended long-term surface. **Descriptor-only
93
+ today — their runners land in C2.** A graph built from them validates and type-checks but raises
94
+ `No runner registered` at execution.
95
+ - **Model runners** (`models/<name>/`) — high-level single generation nodes. **`alibaba/z-image-turbo`
96
+ (`models/zimage/`) is the one runnable generation path today** (prompt + optional image → one take,
97
+ backed by diffusers' `ZImagePipeline`/`ZImageImg2ImgPipeline`). This is the "Z-Image pipeline that
98
+ already works" — the primitives will reach parity in C2. It loads from a **single diffusion
99
+ `.safetensors`** (drop one file in `diffusion_models/`, ComfyUI-style — no repo folder to set up):
100
+ the runner loads the transformer via `from_single_file` and pulls the VAE / text-encoder / tokenizer
101
+ from the reference repo behind the scenes, so the user only ever handles one model file.
102
+ - **Low-level primitives and source nodes are `hidden`** (`NodeDescriptor.hidden`): they are served for
103
+ validation/execution but never offered in the UI's add-node menu. Generation stays one-click — the
104
+ user sees only high-level model nodes; loaders/VAE/encoders are wired up behind them.
105
+
106
+ Only media-output nodes (`vae/decode`, a model runner) become Frames with take history; the engine
107
+ handles (`model`, `vae`, `text-encoder`, `conditioning`, `latent`) are opaque typed sockets passed
108
+ between nodes and are never takes.
109
+
110
+ ### Storage & configuration (all env, see `config.py`)
111
+
112
+ - **Models root** — `INLINE_MODELS_DIR`, else `./models`. **Bring your own weights; nothing is
113
+ downloaded.** ComfyUI-style category subfolders (`diffusion_models/`, `vae/`, `text_encoders/`,
114
+ `loras/`, `controlnet/`, `checkpoints/`, `clip_vision/`, `upscale_models/`, `embeddings/`). The
115
+ catalog scans this on start; a file dropped in bumps the registry version so clients refetch
116
+ `/v1/models`. A model may be a single weight file or a folder (e.g. a diffusers snapshot or a
117
+ sharded text encoder).
118
+ - **Data dir** — `INLINE_DATA_DIR`, else `./.inline`. Engine-owned working data: `runs.db` (durable
119
+ runs) and `takes/` (output bytes).
120
+ - **Server bind** — `INLINE_HOST` (default `127.0.0.1`), `INLINE_PORT` (default `8848`).
121
+ - **Model overrides** — e.g. `INLINE_ZIMAGE_MODEL` (a single `.safetensors` file path, a local
122
+ diffusers dir, or a HF repo id for Z-Image). Auto-resolved from `diffusion_models/` when unset.
123
+ - **Memory** — always prefer the GPU: even under the `lowvram` profile, weights stay resident on the
124
+ GPU (VAE tiling/slicing + attention slicing + int8 do the saving); we do **not** auto-offload to CPU.
125
+ `INLINE_ALLOW_CPU_OFFLOAD=1` opts back into `enable_model_cpu_offload()` for extreme cases.
126
+ - **Multi-GPU** — `INLINE_PARALLEL` (e.g. `pipefusion=2`, `pipefusion=2,ulysses=2`); degrees multiply
127
+ to the world size, which must equal the GPU count.
128
+
129
+ ### The `/v1` API (the contract with Inline Studio)
130
+
131
+ - `POST /v1/runs {graph, target}` → `{runId}` (validated up front; 422 on a bad graph, 409 on a
132
+ reused `clientRunId` with a different graph).
133
+ - `GET /v1/runs/{id}` → durable run state. `DELETE /v1/runs/{id}` cancels.
134
+ - `GET /v1/runs/{id}/events` (websocket) → a `snapshot`, then `progress` / `node_done` / `run_done`.
135
+ - `GET /v1/models` → node descriptors + `registryVersion` (ETag-aware; folds in the model-file scan).
136
+ - `GET /v1/models/{type}`, `GET /v1/takes/{id}`, `GET /v1/takes/{id}/bytes`, `POST /v1/assets`,
137
+ `GET /v1/health`.
138
+
139
+ Errors are `{error: {code, message, nodeId?}}` with the right HTTP status — they never leak a raw
140
+ traceback. The JSON shapes live in `server/serialize.py`.
141
+
142
+ ## Multi-GPU (xDiT): split one image across GPUs
143
+
144
+ One image's **denoise loop** (the expensive part) runs collectively across GPUs — not "one image per
145
+ GPU". It's done with xfuser in an isolated worker group (one process per GPU via `torchrun`, over
146
+ local IPC) behind the `XFuserBatchedSampler` seam. Single-GPU/CPU runs take the in-process path and
147
+ pay no overhead. Split method is chosen from the detected interconnect: **PipeFusion** (default, PCIe)
148
+ or **Ulysses** (NVLink). The policy and IPC round-trip are in place and tested with a stub worker; the
149
+ real xfuser denoise lands with the GPU-side runner (C2). Keep `sampling/batch.py` torch-free — the
150
+ real codec that moves tensors lives with the model runner.
151
+
152
+ ## Code standards (non-negotiable)
153
+
154
+ - **Typed, strict.** `pyright` in strict mode (`[tool.pyright]`, `typeCheckingMode = "strict"`), all of
155
+ `src` + `tests`. No silent `Any` leaks across component/graph boundaries.
156
+ - **Lint.** `ruff` with `select = ["E", "F", "I", "UP", "B"]`, line length 100, target `py311`.
157
+ - **Typed graph, validated before run.** Never execute an unvalidated graph. Edge type-checking
158
+ (`graph/validate.py` + `port_satisfies`) rejects bad wiring at submit. New port kinds go in
159
+ `schema.py`.
160
+ - **Device policy owns placement.** Components and runners **never** self-assign a device/dtype/
161
+ offload — they call `ctx.policy.placement(role)` (`text_encoder`, `denoiser`, `vae`, …). This is the
162
+ rule that keeps one graph portable across GPU / low-VRAM / CPU / multi-GPU. The policy **prefers the
163
+ GPU**: a low-VRAM GPU keeps weights resident (tiling/slicing/int8 do the saving) and does not
164
+ auto-offload to CPU (`placement.offload` defaults False; opt in with `INLINE_ALLOW_CPU_OFFLOAD`).
165
+ - **Graph never runs the denoise inline.** A model runner lowers to components and submits a
166
+ `SampleJob` through the batched-sampler seam. The executor orchestrates; it does not sample.
167
+ - **Immutable takes.** The `TakeStore` owns bytes/hash/uri; regenerating adds a take. Never overwrite.
168
+ - **Engine deps are optional and import-guarded.** Heavy deps (torch, diffusers, xfuser) live in
169
+ `[project.optional-dependencies]` extras (`zimage`, `server`, `parallel`, `dev`). Model-runner
170
+ subpackages import torch/diffusers at module top **on purpose**: an absent extra makes the import
171
+ raise, and `server/bootstrap.py` skips that model best-effort so a core install still boots and
172
+ serves source nodes. Never import a heavy dep at package top level outside a runner subpackage.
173
+ - **Engine isolation.** All ComfyUI-format knowledge lives in `importer/comfy.py`; all xDiT/worker
174
+ knowledge behind `parallel/` and the sampler seam. Don't scatter it.
175
+ - **Bring-your-own models.** Nothing is downloaded by the engine. The catalog scans; the user places
176
+ files. A model picker is a `SELECT` param with `options_from="<category>"`.
177
+ - **Tests (pytest).** Cover the logic that matters: graph validate/topo/executor/cache, the catalog
178
+ scan, the run store + server contract, the device/memory policy, the parallel group + xfuser seam,
179
+ the comfy importer, and each model runner (import-guarded, no GPU needed). See `tests/`.
180
+ - **Commits.** Conventional Commits (`feat:`, `fix:`, `chore:`), small and scoped.
181
+
182
+ ## Commands
183
+
184
+ ```
185
+ uv venv # create ./.venv
186
+ uv pip install -e ".[dev]" # engine + server + test tooling
187
+ uv pip install -e ".[zimage]" # + torch, diffusers, transformers (real generation)
188
+ uv pip install -e ".[parallel]" # + xfuser, for multi-GPU denoise (2+ GPUs)
189
+
190
+ ./webui.sh # run (loopback:8848); friendly flags → INLINE_* env
191
+ ./webui.sh --listen --port 9000 # bind all interfaces
192
+ ./webui.sh --lowvram # tight-VRAM profile
193
+ ./webui.sh --install --extra zimage # set up ./.venv with the Z-Image runtime, then exit
194
+ python -m inline_core.server # run the server directly (INLINE_HOST / INLINE_PORT)
195
+
196
+ ruff check . # lint (zero warnings)
197
+ uv run pytest -q # tests (no GPU; model code is import-guarded)
198
+ ```
199
+
200
+ ## Where to add things
201
+
202
+ - **New model runner** → a subpackage `models/<name>/` with `runner.py` (a `NodeDescriptor` + a
203
+ `NodeRunner` + `register_<name>(registry, store, policy)`) and an `__init__.py` re-exporting it;
204
+ add a `try/except ImportError` block in `server/bootstrap.py`; add an optional-deps extra in
205
+ `pyproject.toml`. Copy `models/zimage/` — it's the reference.
206
+ - **New low-level primitive** → descriptor in `graph/primitives.py`; its runner lands with the C2 work
207
+ (build a component in `components/`, wire it through `encode`/`sample`/`vae` and the sampler seam).
208
+ - **New `/v1` route** → add it in `server/app.py`, shape the JSON in `server/serialize.py`, keep errors
209
+ as `{error:{code,message}}` with the right status. Update the API list in `README.md`.
210
+ - **New port/handle type** → `PortKind` in `graph/schema.py` (+ `port_satisfies` if it has coercions).
211
+ - **New device/memory behaviour** → behind `DevicePolicy` in `device/`; never in a component.
212
+ - **New ComfyUI import behaviour** → `importer/comfy.py` only.
@@ -0,0 +1,35 @@
1
+ Metadata-Version: 2.4
2
+ Name: inline-core
3
+ Version: 1.1.1
4
+ Summary: The generation engine behind Inline.
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: numpy>=1.26
7
+ Requires-Dist: psutil>=5.9
8
+ Provides-Extra: dev
9
+ Requires-Dist: fastapi>=0.110; extra == 'dev'
10
+ Requires-Dist: httpx>=0.27; extra == 'dev'
11
+ Requires-Dist: imageio-ffmpeg>=0.4; extra == 'dev'
12
+ Requires-Dist: pyright>=1.1; extra == 'dev'
13
+ Requires-Dist: pytest>=8; extra == 'dev'
14
+ Requires-Dist: ruff>=0.6; extra == 'dev'
15
+ Requires-Dist: uvicorn[standard]>=0.29; extra == 'dev'
16
+ Provides-Extra: parallel
17
+ Requires-Dist: accelerate>=0.30; extra == 'parallel'
18
+ Requires-Dist: diffusers>=0.36; extra == 'parallel'
19
+ Requires-Dist: huggingface-hub>=0.23; extra == 'parallel'
20
+ Requires-Dist: nvidia-ml-py>=12; extra == 'parallel'
21
+ Requires-Dist: safetensors>=0.4; extra == 'parallel'
22
+ Requires-Dist: torch>=2.2; extra == 'parallel'
23
+ Requires-Dist: transformers>=4.44; extra == 'parallel'
24
+ Requires-Dist: xfuser>=0.4; extra == 'parallel'
25
+ Provides-Extra: server
26
+ Requires-Dist: fastapi>=0.110; extra == 'server'
27
+ Requires-Dist: imageio-ffmpeg>=0.4; extra == 'server'
28
+ Requires-Dist: uvicorn[standard]>=0.29; extra == 'server'
29
+ Provides-Extra: zimage
30
+ Requires-Dist: accelerate>=0.30; extra == 'zimage'
31
+ Requires-Dist: diffusers>=0.36; extra == 'zimage'
32
+ Requires-Dist: huggingface-hub>=0.23; extra == 'zimage'
33
+ Requires-Dist: safetensors>=0.4; extra == 'zimage'
34
+ Requires-Dist: torch>=2.2; extra == 'zimage'
35
+ Requires-Dist: transformers>=4.44; extra == 'zimage'
@@ -0,0 +1,177 @@
1
+ # Inline Core
2
+
3
+ The generation engine behind Inline. It takes a typed node graph (JSON) and returns immutable renders
4
+ ("takes"), running image and video models on macOS, Windows, and Linux, from CPU-only boxes and
5
+ low-VRAM laptops up to multi-GPU machines that split a single image's sampling across GPUs (via
6
+ xDiT). It is the render backend that replaces ComfyUI for Inline.
7
+
8
+ First model: Z-Image (Alibaba Tongyi), a 6B rectified-flow diffusion transformer (and a model xDiT
9
+ already supports, so the multi-GPU split works on it from the start).
10
+
11
+ > Status: early, and running end to end against a stub engine. In place and tested: the graph engine,
12
+ > the typed `/v1` HTTP + websocket API (durable runs, streamed progress, coalescing), the model-dir
13
+ > scan, the device + memory policy (profiles, dtype, offload, int8), the low-level primitive node
14
+ > vocabulary, and a ComfyUI workflow importer. The Z-Image loader is written and validates on a GPU.
15
+ > Cross-request batching, single-image multi-GPU (an xDiT worker group behind the sampler seam, with
16
+ > the policy and IPC round-trip tested), and out-of-process custom nodes are built as seams but not
17
+ > yet running on real hardware.
18
+
19
+ ## How it differs from ComfyUI's architecture
20
+
21
+ ComfyUI is a great canvas but a fragile engine. Inline Core keeps the open node-graph model and
22
+ rebuilds the engine underneath it.
23
+
24
+ | | ComfyUI | Inline Core |
25
+ | ------------ | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
26
+ | Graph vs GPU | runs the denoise loop inline, one request at a time | graph orchestration (cheap, per request) is separate from a batched sampler that groups compatible jobs across requests |
27
+ | Schema | positional `widgets_values`, validated at runtime (dies mid-graph) | typed graph, named params, edges type-checked before the run (rejected at submit) |
28
+ | Devices | some nodes pin to CPU on a GPU box; Z-Image will not run on CPU | one device/memory policy owns dtype, placement, offload, and attention; no node hardcodes a device, so one graph runs GPU, low-VRAM, or pure CPU |
29
+ | Multi-GPU | one image runs on one GPU; splitting a single image needs third-party nodes | one image's denoise can split across GPUs via xDiT (PipeFusion on PCIe, Ulysses on NVLink), in an isolated worker group behind the sampler seam |
30
+ | Custom nodes | all load into one interpreter and env, so any node can break the core | run out of process, each pack in its own venv, behind a semver SDK |
31
+ | Interface | a web UI driven by graph JSON over a socket; run state is ephemeral | a headless `/v1` HTTP + websocket API; runs are durable and survive a restart |
32
+ | Outputs | files you overwrite | immutable takes; regenerating adds a take, never overwrites |
33
+ | Models | `models/` dir, dropdowns from a scan | same layout (bring your own, no downloads); a typed catalog feeds versioned node descriptors the UI renders generically |
34
+
35
+ The two boundaries that matter most: graph orchestration is decoupled from GPU batching (graphs are
36
+ the unit of caching, the sampler is the unit of batching, and the multi-GPU split routes through that
37
+ same seam), and the device policy is the single owner of placement, so the same graph runs on a 4090,
38
+ a 6 GB laptop, pure CPU, or split across several GPUs, without touching the graph.
39
+
40
+ ## Install
41
+
42
+ Requires Python 3.11+ and [uv](https://docs.astral.sh/uv/).
43
+
44
+ ```
45
+ uv venv
46
+ uv pip install -e ".[server]" # engine + HTTP/websocket API
47
+ uv pip install -e ".[zimage]" # + torch, diffusers, transformers (for real generation)
48
+ uv pip install -e ".[parallel]" # + xfuser, for splitting one image across GPUs (needs 2+ GPUs)
49
+ ```
50
+
51
+ ## Models
52
+
53
+ Drop files into the models dir (default `./models`, override with `INLINE_MODELS_DIR`), ComfyUI-style,
54
+ by category:
55
+
56
+ ```
57
+ models/
58
+ diffusion_models/ z_image_turbo_bf16.safetensors <- the one file you need for Z-Image
59
+ vae/ ae.safetensors <- optional (see below)
60
+ text_encoders/ qwen/ (an HF-format folder: config + tokenizer + weights) <- optional
61
+ loras/ controlnet/ checkpoints/ ...
62
+ ```
63
+
64
+ **Z-Image loads from a single diffusion `.safetensors`.** Drop that one ComfyUI-style file in
65
+ `diffusion_models/`; the runner loads the transformer via diffusers `from_single_file`. It also needs
66
+ a VAE + text-encoder + tokenizer, resolved from **local files** — `vae/*.safetensors` or a dir, and an
67
+ HF-format `text_encoders/` dir.
68
+
69
+ **Nothing is ever downloaded as a side effect of a render.** Every load runs `local_files_only=True`,
70
+ so a missing model is a clear, fast error, not a silent multi-GB fetch. Models arrive by exactly two
71
+ paths:
72
+
73
+ 1. **You place files** under `models/` (bring-your-own / fully offline); or
74
+ 2. **You download them explicitly** from the Z-Image node's model popup in the UI — it lists the
75
+ diffusion model, VAE, and text-encoder, shows which are missing, and downloads them **into
76
+ `models/`** (never the hidden HF cache) with visible progress.
77
+
78
+ Override the diffusion source with `INLINE_ZIMAGE_MODEL` (a file or a diffusers dir), and the supporting
79
+ components with `INLINE_ZIMAGE_VAE` / `INLINE_ZIMAGE_TEXT_ENCODER`. The engine scans the models dir on
80
+ start; a node's model pickers list what is present.
81
+
82
+ ## Nodes
83
+
84
+ `/v1/models` serves each node's descriptor (ports, params, file pickers), so the Inline Studio canvas
85
+ renders any node generically — adding a node type needs no UI release.
86
+
87
+ **High-level model nodes are what the user sees.** Generation is one-click: you drop a single
88
+ **Z-Image Turbo** node, wire a Prompt into it, and hit Run. The node hooks up the diffusion model, VAE,
89
+ and text-encoder behind the scenes — no loader/sampler wiring.
90
+
91
+ Underneath, a **low-level primitive vocabulary** exists (`load/diffusion-model`, `load/vae`,
92
+ `load/text-encoder`, `encode/text`, `latent/empty`, `sample`, `vae/decode`, `vae/encode`) — the
93
+ ComfyUI-equivalent decomposed graph, kept for validation/execution — but these are marked **`hidden`**
94
+ and never appear in the add-node menu. Engine handles (`model`, `vae`, `text-encoder`, `conditioning`,
95
+ `latent`) are typed sockets between nodes; only media outputs become Frames with take history. A
96
+ best-effort ComfyUI importer maps existing workflows onto the primitives.
97
+
98
+ ## Multi-GPU: split one image across GPUs
99
+
100
+ Cut a single image's latency by running its denoise across several GPUs. The denoise loop (the
101
+ iterative sampling step) is the expensive part of a render; with two or more GPUs, Inline Core runs
102
+ each step's transformer forward collectively across them so one image finishes faster. This is not
103
+ "one image per GPU" (independent renders); it is one image whose sampling is shared by all the GPUs.
104
+
105
+ It is done with xDiT (`xfuser`), which parallelizes diffusion-transformer inference. xfuser runs in
106
+ an isolated worker group the engine spawns (one process per GPU via `torchrun`) and talks to over
107
+ local IPC, so the HTTP server, database, and graph stay single-process and only the denoise is
108
+ distributed. It sits behind the sampler seam (`XFuserBatchedSampler`), so a single-GPU or CPU run
109
+ takes the in-process path and pays no overhead.
110
+
111
+ Two split methods, chosen from the interconnect the engine detects:
112
+
113
+ - **PipeFusion (default, PCIe):** shards the transformer into a displaced patch pipeline with low,
114
+ depth-independent communication. It needs no NVLink and works over plain PCIe (or Ethernet across
115
+ nodes), so it is the default on a typical multi-GPU box.
116
+ - **Ulysses (NVLink):** sequence-parallel attention, used when NVLink is present because it wants
117
+ the higher interconnect bandwidth.
118
+
119
+ Enabling it:
120
+
121
+ 1. **Install the extra** and have 2+ CUDA GPUs on one machine:
122
+ ```
123
+ uv pip install -e ".[parallel]" # pulls in xfuser and nvidia-ml-py; torchrun ships with torch
124
+ ```
125
+ 2. **Run normally.** On the first denoise, the device policy enumerates the GPUs, detects NVLink vs
126
+ PCIe (via `nvidia-ml-py`), and returns a parallel placement when there is more than one GPU. The
127
+ engine then spawns the xfuser worker group (lazily, once, then reuses it) and splits the sampling
128
+ across the GPUs. No graph, API, or per-request change is needed.
129
+ 3. **Override the split** if you want to pick it by hand, with `INLINE_PARALLEL`:
130
+ ```
131
+ INLINE_PARALLEL=pipefusion=2 # 2 GPUs, PipeFusion
132
+ INLINE_PARALLEL=pipefusion=2,ulysses=2 # 4 GPUs, PipeFusion x Ulysses
133
+ ```
134
+ The degrees multiply to the world size, which must equal the number of GPUs.
135
+
136
+ The device policy and the worker-group IPC are in place and tested with a stub worker; the real
137
+ xfuser denoise lands with the GPU-side Z-Image runner.
138
+
139
+ ## Run
140
+
141
+ The easy path is `webui.sh`, which maps friendly flags onto the engine's `INLINE_*` env knobs:
142
+
143
+ ```
144
+ ./webui.sh # loopback, port 8848
145
+ ./webui.sh --listen --port 9000 # bind all interfaces on 9000
146
+ ./webui.sh --multi-gpu # split one image across GPUs (auto with 2+ GPUs)
147
+ ./webui.sh --lowvram # tight-VRAM profile
148
+ ./webui.sh --install --extra zimage # set up ./.venv with the Z-Image runtime, then exit
149
+ ```
150
+
151
+ `./webui.sh --help` lists every flag (networking, multi-GPU, device/memory profile, paths). The same
152
+ flags are available on the Python entrypoint — `python main.py --help` — which is the dev path (it also
153
+ takes `--front-end-root DIR` to serve a local SPA build). Or run the server module directly:
154
+
155
+ ```
156
+ python main.py --listen --port 9000 # same flags as webui.sh; --front-end-root for a local UI build
157
+ python -m inline_core.server # bare server (INLINE_HOST / INLINE_PORT from the env)
158
+ ```
159
+
160
+ Working data (the run database and takes) lives in `INLINE_DATA_DIR` (default `./.inline`).
161
+
162
+ ## API (v1)
163
+
164
+ - `POST /v1/runs {graph, target}` returns `{runId}` (validated up front; 422 on a bad graph)
165
+ - `GET /v1/runs/{id}` returns run state (durable; survives a restart)
166
+ - `GET /v1/runs/{id}/events` (websocket): a snapshot, then `progress` / `node_done` / `run_done`
167
+ - `DELETE /v1/runs/{id}` cancels
168
+ - `GET /v1/models` returns node descriptors + `registryVersion` (ETag-aware)
169
+ - `GET /v1/takes/{id}` and `/v1/takes/{id}/bytes`
170
+ - `POST /v1/assets` (content-addressed upload) and `GET /v1/health`
171
+
172
+ ## Development
173
+
174
+ ```
175
+ ruff check . # lint
176
+ uv run pytest -q # tests (no GPU needed; model code is import-guarded)
177
+ ```
@@ -0,0 +1,93 @@
1
+ """Inline Studio — single entrypoint.
2
+
3
+ Run one Python process that serves both the API and the UI on one port:
4
+
5
+ python main.py # end users: frontend from the installed package
6
+ python main.py --front-end-root ../Inline-Studio/dist-web # dev: serve a local SPA build
7
+ python main.py --listen --port 8000 # bind 0.0.0.0 on a custom port
8
+
9
+ Flags map onto the engine's INLINE_* env knobs, then hand off to the server entrypoint. Build the
10
+ dev frontend with `npm run build:spa` in the Inline Studio repo, or use `npm run dev:web` for HMR.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import os
17
+
18
+
19
+ def main() -> None:
20
+ parser = argparse.ArgumentParser(
21
+ prog="inline-studio",
22
+ description="Run Inline Core + the Inline Studio UI on one port.",
23
+ )
24
+ # Networking
25
+ parser.add_argument("--host", help="Bind address (default 127.0.0.1).")
26
+ parser.add_argument(
27
+ "--listen",
28
+ action="store_true",
29
+ help="Bind 0.0.0.0 so other machines on your network can reach it.",
30
+ )
31
+ parser.add_argument("--port", type=int, help="Port to serve on (default 8848).")
32
+ # Paths
33
+ parser.add_argument(
34
+ "--front-end-root",
35
+ help="Serve a local frontend build (a dir with index.html) instead of the installed "
36
+ "inline_studio_frontend package. The dev loop — rebuild locally, no republish.",
37
+ )
38
+ parser.add_argument("--models-dir", help="Models root to scan (default ./models).")
39
+ parser.add_argument("--data-dir", help="Working data dir: runs + takes (default ./.inline).")
40
+ # Device / memory
41
+ parser.add_argument(
42
+ "--profile", help="Device/memory profile: gpu-max | lowvram | cpu (default: auto-detected)."
43
+ )
44
+ parser.add_argument(
45
+ "--lowvram", action="store_true", help="Shortcut for --profile lowvram (tight-VRAM saving)."
46
+ )
47
+ parser.add_argument(
48
+ "--cpu", action="store_true", help="Shortcut for --profile cpu (force CPU generation)."
49
+ )
50
+ parser.add_argument(
51
+ "--vram-budget", type=float, help="Treat the GPU as having this many GB of usable VRAM."
52
+ )
53
+ # Multi-GPU (xDiT): split one image's denoise across GPUs. Auto-detected with 2+ GPUs.
54
+ parser.add_argument(
55
+ "--multi-gpu",
56
+ "--parallel",
57
+ dest="parallel",
58
+ help="Multi-GPU split spec, e.g. pipefusion=2 or pipefusion=2,ulysses=2 (degrees multiply "
59
+ "to the GPU count). Auto-detected with 2+ GPUs.",
60
+ )
61
+ args = parser.parse_args()
62
+
63
+ if args.listen:
64
+ os.environ["INLINE_HOST"] = "0.0.0.0"
65
+ if args.host:
66
+ os.environ["INLINE_HOST"] = args.host
67
+ if args.port:
68
+ os.environ["INLINE_PORT"] = str(args.port)
69
+ if args.front_end_root:
70
+ os.environ["INLINE_FRONTEND_ROOT"] = args.front_end_root
71
+ if args.models_dir:
72
+ os.environ["INLINE_MODELS_DIR"] = args.models_dir
73
+ if args.data_dir:
74
+ os.environ["INLINE_DATA_DIR"] = args.data_dir
75
+ if args.lowvram:
76
+ os.environ["INLINE_PROFILE"] = "lowvram"
77
+ if args.cpu:
78
+ os.environ["INLINE_PROFILE"] = "cpu"
79
+ if args.profile:
80
+ os.environ["INLINE_PROFILE"] = args.profile
81
+ if args.vram_budget:
82
+ os.environ["INLINE_VRAM_BUDGET_GB"] = str(args.vram_budget)
83
+ if args.parallel:
84
+ os.environ["INLINE_PARALLEL"] = args.parallel
85
+
86
+ # Import after env is set so config picks it up.
87
+ from inline_core.server.__main__ import main as run_server
88
+
89
+ run_server()
90
+
91
+
92
+ if __name__ == "__main__":
93
+ main()
@@ -0,0 +1,82 @@
1
+ [project]
2
+ # PyPI distribution name. The import package is `inline_core` (src/inline_core); PyPI treats the
3
+ # hyphen and underscore as equivalent, so `pip install inline-core` installs the `inline_core` package.
4
+ name = "inline-core"
5
+ version = "1.1.1"
6
+ description = "The generation engine behind Inline."
7
+ requires-python = ">=3.11"
8
+ dependencies = [
9
+ "numpy>=1.26",
10
+ "psutil>=5.9",
11
+ ]
12
+
13
+ [project.optional-dependencies]
14
+ # Model-runtime deps land with the component implementations. The installer selects the right
15
+ # torch wheel per OS and accelerator (see PLAN.md, cross-platform). Kept out of the core env so
16
+ # the engine skeleton stays light and portable to review.
17
+ zimage = [
18
+ "torch>=2.2",
19
+ # Z-Image landed in diffusers 0.36 (ZImagePipeline); diffusers from git main may be required.
20
+ "diffusers>=0.36",
21
+ "transformers>=4.44",
22
+ "accelerate>=0.30",
23
+ "safetensors>=0.4",
24
+ # Explicit model downloads (the node's model popup) go through huggingface_hub. It ships
25
+ # transitively with diffusers/transformers, but pin it here since we call snapshot_download
26
+ # directly and rely on local_dir= to land files in models/ (never the hidden HF cache).
27
+ "huggingface_hub>=0.23",
28
+ ]
29
+ server = [
30
+ "fastapi>=0.110",
31
+ "uvicorn[standard]>=0.29",
32
+ # Bundled ffmpeg binary for the Studio timeline render (director preview/export). ffprobe is not
33
+ # bundled — install a system ffmpeg for duration probing, else clips fall back to a still length.
34
+ "imageio-ffmpeg>=0.4",
35
+ ]
36
+ # The prebuilt web UI (inline-studio-frontend) is NOT a hard dependency here on purpose: it's
37
+ # published separately by CI and a source/dev checkout serves the UI via `--front-end-root ../dist-web`
38
+ # with no wheel at all. End users get it through the pip path — `requirements.txt` (repo root) pins
39
+ # `inline-studio-frontend` alongside the engine (mirrors ComfyUI's requirements.txt), and `webui.sh`
40
+ # installs it on demand. Core's resolve_frontend_root() then finds the installed package's static/.
41
+ # Multi-GPU denoise: split one image across GPUs via xDiT (xfuser). Isolated in the worker group so
42
+ # the core env is unaffected. torchrun ships with torch (from zimage); nvidia-ml-py detects NVLink so
43
+ # the policy picks Ulysses vs PipeFusion. flash-attn (Ulysses/Ring on NVLink) is an opt-in extra.
44
+ parallel = [
45
+ "inline-core[zimage]",
46
+ "xfuser>=0.4",
47
+ "nvidia-ml-py>=12",
48
+ ]
49
+ dev = [
50
+ "inline-core[server]",
51
+ "pytest>=8",
52
+ "ruff>=0.6",
53
+ "pyright>=1.1",
54
+ "httpx>=0.27",
55
+ ]
56
+
57
+ [project.scripts]
58
+ # One command to run the engine + the Inline Studio UI on one port (see main.py for flags).
59
+ inline-studio = "inline_core.server.__main__:main"
60
+
61
+ [build-system]
62
+ requires = ["hatchling"]
63
+ build-backend = "hatchling.build"
64
+
65
+ [tool.hatch.build.targets.wheel]
66
+ packages = ["src/inline_core"]
67
+
68
+ [tool.ruff]
69
+ line-length = 100
70
+ target-version = "py311"
71
+
72
+ [tool.ruff.lint]
73
+ select = ["E", "F", "I", "UP", "B"]
74
+
75
+ [tool.pyright]
76
+ include = ["src", "tests"]
77
+ pythonVersion = "3.11"
78
+ typeCheckingMode = "strict"
79
+
80
+ [tool.pytest.ini_options]
81
+ testpaths = ["tests"]
82
+ pythonpath = ["src"]