inline-core 1.1.2__tar.gz → 1.2.0__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 (130) hide show
  1. {inline_core-1.1.2 → inline_core-1.2.0}/.gitignore +1 -1
  2. {inline_core-1.1.2 → inline_core-1.2.0}/CLAUDE.md +49 -47
  3. inline_core-1.2.0/PKG-INFO +247 -0
  4. {inline_core-1.1.2 → inline_core-1.2.0}/README.md +27 -30
  5. {inline_core-1.1.2 → inline_core-1.2.0}/main.py +2 -2
  6. inline_core-1.2.0/pyproject.toml +96 -0
  7. inline_core-1.2.0/src/inline_core/device/detect.py +118 -0
  8. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/device/memory.py +15 -15
  9. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/device/policy.py +4 -4
  10. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/graph/descriptor.py +1 -1
  11. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/graph/loader_runners.py +3 -3
  12. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/graph/primitives.py +1 -1
  13. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/graph/registry.py +1 -1
  14. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/models/loaders.py +25 -25
  15. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/models/sampling.py +9 -9
  16. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/models/zimage/primitives.py +7 -7
  17. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/models/zimage/requirements.py +13 -13
  18. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/models/zimage/runner.py +49 -49
  19. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/server/__main__.py +7 -1
  20. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/server/app.py +14 -5
  21. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/server/frontend.py +4 -4
  22. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/server/rpc.py +2 -2
  23. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/studio/__init__.py +2 -2
  24. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/studio/assets.py +3 -3
  25. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/studio/config.py +2 -2
  26. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/studio/fal.py +123 -6
  27. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/studio/generation.py +3 -3
  28. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/studio/graph_build.py +1 -1
  29. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/studio/models.py +7 -7
  30. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/studio/moodboard.py +2 -2
  31. inline_core-1.2.0/src/inline_core/studio/peaks.py +170 -0
  32. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/studio/schema.py +1 -1
  33. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/studio/store.py +6 -3
  34. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/studio/timeline/compose.py +1 -1
  35. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/studio/timeline/resolve.py +11 -3
  36. inline_core-1.2.0/tests/test_device_detect.py +126 -0
  37. {inline_core-1.1.2 → inline_core-1.2.0}/tests/test_hidden_nodes.py +1 -1
  38. {inline_core-1.1.2 → inline_core-1.2.0}/tests/test_loader_runners.py +1 -1
  39. {inline_core-1.1.2 → inline_core-1.2.0}/tests/test_loaders.py +3 -3
  40. {inline_core-1.1.2 → inline_core-1.2.0}/tests/test_memory_policy.py +11 -5
  41. inline_core-1.2.0/tests/test_studio_fal.py +190 -0
  42. {inline_core-1.1.2 → inline_core-1.2.0}/tests/test_studio_generation.py +2 -2
  43. {inline_core-1.1.2 → inline_core-1.2.0}/tests/test_studio_models.py +1 -1
  44. inline_core-1.2.0/tests/test_studio_peaks.py +143 -0
  45. {inline_core-1.1.2 → inline_core-1.2.0}/tests/test_studio_rpc.py +1 -1
  46. {inline_core-1.1.2 → inline_core-1.2.0}/tests/test_zimage_primitives.py +4 -4
  47. {inline_core-1.1.2 → inline_core-1.2.0}/tests/test_zimage_resolve.py +3 -3
  48. {inline_core-1.1.2 → inline_core-1.2.0}/tests/test_zimage_runner.py +7 -7
  49. {inline_core-1.1.2 → inline_core-1.2.0}/uv.lock +112 -45
  50. {inline_core-1.1.2 → inline_core-1.2.0}/webui.sh +21 -11
  51. inline_core-1.1.2/PKG-INFO +0 -39
  52. inline_core-1.1.2/pyproject.toml +0 -91
  53. inline_core-1.1.2/src/inline_core/device/detect.py +0 -57
  54. inline_core-1.1.2/tests/test_studio_fal.py +0 -63
  55. {inline_core-1.1.2 → inline_core-1.2.0}/.python-version +0 -0
  56. {inline_core-1.1.2 → inline_core-1.2.0}/scripts/reference.py +0 -0
  57. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/__init__.py +0 -0
  58. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/components/__init__.py +0 -0
  59. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/components/conditioning.py +0 -0
  60. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/components/interfaces.py +0 -0
  61. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/config.py +0 -0
  62. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/device/__init__.py +0 -0
  63. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/device/auto.py +0 -0
  64. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/device/types.py +0 -0
  65. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/errors.py +0 -0
  66. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/graph/__init__.py +0 -0
  67. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/graph/cache.py +0 -0
  68. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/graph/executor.py +0 -0
  69. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/graph/runners.py +0 -0
  70. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/graph/schema.py +0 -0
  71. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/graph/topo.py +0 -0
  72. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/graph/validate.py +0 -0
  73. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/importer/__init__.py +0 -0
  74. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/importer/comfy.py +0 -0
  75. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/media.py +0 -0
  76. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/models/__init__.py +0 -0
  77. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/models/catalog.py +0 -0
  78. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/models/zimage/__init__.py +0 -0
  79. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/parallel/__init__.py +0 -0
  80. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/parallel/config.py +0 -0
  81. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/parallel/group.py +0 -0
  82. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/parallel/launch.py +0 -0
  83. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/parallel/protocol.py +0 -0
  84. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/parallel/registry.py +0 -0
  85. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/parallel/worker.py +0 -0
  86. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/runtime/__init__.py +0 -0
  87. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/runtime/context.py +0 -0
  88. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/runtime/file_store.py +0 -0
  89. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/runtime/progress.py +0 -0
  90. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/runtime/run.py +0 -0
  91. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/runtime/store.py +0 -0
  92. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/sampling/__init__.py +0 -0
  93. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/sampling/batch.py +0 -0
  94. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/server/__init__.py +0 -0
  95. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/server/assets.py +0 -0
  96. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/server/bootstrap.py +0 -0
  97. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/server/manager.py +0 -0
  98. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/server/run_store.py +0 -0
  99. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/server/serialize.py +0 -0
  100. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/studio/frames.py +0 -0
  101. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/studio/handlers.py +0 -0
  102. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/studio/timeline/__init__.py +0 -0
  103. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/studio/timeline/ffmpeg.py +0 -0
  104. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/studio/timeline/render.py +0 -0
  105. {inline_core-1.1.2 → inline_core-1.2.0}/src/inline_core/takes.py +0 -0
  106. {inline_core-1.1.2 → inline_core-1.2.0}/tests/helpers.py +0 -0
  107. {inline_core-1.1.2 → inline_core-1.2.0}/tests/test_cache.py +0 -0
  108. {inline_core-1.1.2 → inline_core-1.2.0}/tests/test_catalog.py +0 -0
  109. {inline_core-1.1.2 → inline_core-1.2.0}/tests/test_comfy_import.py +0 -0
  110. {inline_core-1.1.2 → inline_core-1.2.0}/tests/test_config.py +0 -0
  111. {inline_core-1.1.2 → inline_core-1.2.0}/tests/test_executor.py +0 -0
  112. {inline_core-1.1.2 → inline_core-1.2.0}/tests/test_file_store.py +0 -0
  113. {inline_core-1.1.2 → inline_core-1.2.0}/tests/test_frontend_serving.py +0 -0
  114. {inline_core-1.1.2 → inline_core-1.2.0}/tests/test_parallel_group.py +0 -0
  115. {inline_core-1.1.2 → inline_core-1.2.0}/tests/test_primitives.py +0 -0
  116. {inline_core-1.1.2 → inline_core-1.2.0}/tests/test_rpc_bridge.py +0 -0
  117. {inline_core-1.1.2 → inline_core-1.2.0}/tests/test_run_store.py +0 -0
  118. {inline_core-1.1.2 → inline_core-1.2.0}/tests/test_sampling.py +0 -0
  119. {inline_core-1.1.2 → inline_core-1.2.0}/tests/test_schema.py +0 -0
  120. {inline_core-1.1.2 → inline_core-1.2.0}/tests/test_server.py +0 -0
  121. {inline_core-1.1.2 → inline_core-1.2.0}/tests/test_studio_assets.py +0 -0
  122. {inline_core-1.1.2 → inline_core-1.2.0}/tests/test_studio_frames.py +0 -0
  123. {inline_core-1.1.2 → inline_core-1.2.0}/tests/test_studio_moodboard.py +0 -0
  124. {inline_core-1.1.2 → inline_core-1.2.0}/tests/test_studio_schema.py +0 -0
  125. {inline_core-1.1.2 → inline_core-1.2.0}/tests/test_studio_store.py +0 -0
  126. {inline_core-1.1.2 → inline_core-1.2.0}/tests/test_studio_timeline.py +0 -0
  127. {inline_core-1.1.2 → inline_core-1.2.0}/tests/test_take_bytes.py +0 -0
  128. {inline_core-1.1.2 → inline_core-1.2.0}/tests/test_topo.py +0 -0
  129. {inline_core-1.1.2 → inline_core-1.2.0}/tests/test_validate.py +0 -0
  130. {inline_core-1.1.2 → inline_core-1.2.0}/tests/test_xfuser_sampler.py +0 -0
@@ -1,4 +1,4 @@
1
- # Model weights (gigabytes). The engine's models root `models_dir()` defaults to ./models, and
1
+ # Model weights (gigabytes). The engine's models root - `models_dir()` defaults to ./models, and
2
2
  # runners scan ./models/<category>/ (e.g. diffusion_models/). Leading slash anchors this to the repo
3
3
  # root ONLY, so the source package src/inline_core/models/ (code) stays tracked.
4
4
  /models/
@@ -1,8 +1,8 @@
1
- # Inline Core Engineering Guide
1
+ # Inline Core - Engineering Guide
2
2
 
3
3
  Inline Core is the **generation engine behind Inline** (the Storyline / Inline Studio UI client). It
4
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
5
+ models across macOS, Windows, and Linux - from CPU-only boxes and low-VRAM laptops up to multi-GPU
6
6
  machines that split a single image's sampling across GPUs (via xDiT). **It is the render backend that
7
7
  replaces ComfyUI for Inline.**
8
8
 
@@ -19,20 +19,20 @@ replaces ComfyUI for Inline.**
19
19
  Graph (typed nodes + edges) → Run → Take[] (immutable renders)
20
20
  ```
21
21
 
22
- - **Graph** a JSON DAG of typed nodes. Edges are type-checked (`model`, `vae`, `conditioning`,
22
+ - **Graph** - a JSON DAG of typed nodes. Edges are type-checked (`model`, `vae`, `conditioning`,
23
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
24
+ - **Run** - one execution of a target node's upstream closure. Durable (survives a restart) and
25
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
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
29
  `/v1/models`) and a **runner** (the behavior half). A descriptor with no runner is served and
30
30
  type-checked but cannot execute yet.
31
31
 
32
32
  ### The two boundaries that matter most (why this isn't ComfyUI)
33
33
 
34
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
35
+ and never runs the denoise loop itself - a model runner submits a `SampleJob` through the
36
36
  **batched-sampler seam** (`sampling/batch.py`). The graph is the unit of caching; the sampler is
37
37
  the unit of batching; the multi-GPU split routes through that same seam.
38
38
  2. **The device policy is the single owner of placement.** No node or component ever picks a device,
@@ -40,7 +40,7 @@ Graph (typed nodes + edges) → Run → Take[] (immutable renders)
40
40
  6 GB laptop, pure CPU, or split across several GPUs, without touching the graph.
41
41
 
42
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.
43
+ executor, stop - you're breaking one of the two boundaries the whole design exists to keep.
44
44
 
45
45
  ## Architecture
46
46
 
@@ -54,53 +54,53 @@ HTTP/WS → server/app.py → RunManager → Executor → Registry (desc
54
54
  SampleJob → BatchedSampler (inline | xDiT worker group)
55
55
  ```
56
56
 
57
- - **`server/`** the `/v1` API. `app.py` (routes), `manager.py` (validate → queue → run on a worker
57
+ - **`server/`** - the `/v1` API. `app.py` (routes), `manager.py` (validate → queue → run on a worker
58
58
  thread → fan out events), `run_store.py` (SQLite durability), `bootstrap.py` (best-effort model
59
59
  registration), `serialize.py` (contract JSON), `assets.py` (content-addressed upload).
60
- - **`graph/`** the engine core. `schema.py` (typed `Graph`/`Node`/`Edge` + JSON parser),
60
+ - **`graph/`** - the engine core. `schema.py` (typed `Graph`/`Node`/`Edge` + JSON parser),
61
61
  `descriptor.py` (node data half), `runners.py` (node behavior half + source nodes), `registry.py`
62
62
  (descriptors + runners), `validate.py` + `topo.py` (type-check + order), `executor.py` (lazy
63
63
  closure execution, node cache), `primitives.py` (the low-level node vocabulary), `cache.py`.
64
- - **`components/`** the five device-agnostic component interfaces (`TextEncoder`, `Scheduler`,
64
+ - **`components/`** - the five device-agnostic component interfaces (`TextEncoder`, `Scheduler`,
65
65
  `Denoiser`, `Sampler`, `VAE`) plus opaque `Conditioning`/`Latents`. Placement comes from the ctx.
66
- - **`sampling/`** `batch.py`: the graph/GPU boundary. `SampleJob` → `BatchedSampler`
66
+ - **`sampling/`** - `batch.py`: the graph/GPU boundary. `SampleJob` → `BatchedSampler`
67
67
  (`InlineBatchedSampler` today; `XFuserBatchedSampler` routes a parallel placement to the worker
68
68
  group). Keep this module torch-free and mockable.
69
- - **`device/`** the policy. `policy.py` (interface: `Placement`, `Profile`, quant, attention),
69
+ - **`device/`** - the policy. `policy.py` (interface: `Placement`, `Profile`, quant, attention),
70
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
71
+ - **`parallel/`** - the xDiT (xfuser) worker group: one process per GPU via `torchrun`, talking over
72
72
  local IPC behind the sampler seam. `launch.py`, `worker.py`, `group.py`, `registry.py`, `config.py`,
73
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
74
+ - **`models/`** - `catalog.py` (scans the models root, feeds `options_from` selects + the registry
75
75
  version) and the **model-runner subpackages** (e.g. `zimage/`), imported best-effort by
76
76
  `server/bootstrap.py` so a torch-less install still boots.
77
- - **`runtime/`** `context.py` (`ExecutionContext`, `CancelToken`), `run.py` (`RunState`),
77
+ - **`runtime/`** - `context.py` (`ExecutionContext`, `CancelToken`), `run.py` (`RunState`),
78
78
  `progress.py` (events + emitters), `store.py` / `file_store.py` (`TakeStore`: owns take bytes/hash/
79
79
  uri).
80
- - **`importer/`** `comfy.py`: best-effort ComfyUI-workflow → Inline-Core-graph mapping. All ComfyUI
80
+ - **`importer/`** - `comfy.py`: best-effort ComfyUI-workflow → Inline-Core-graph mapping. All ComfyUI
81
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).
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
84
 
85
85
  ### Node vocabularies (three, and their status)
86
86
 
87
- - **Source nodes** (`graph/runners.py`) `input/text`, `input/image`. Runners exist; pure, no takes.
87
+ - **Source nodes** (`graph/runners.py`) - `input/text`, `input/image`. Runners exist; pure, no takes.
88
88
  These are the closure boundary: the UI feeds curated inputs in as source nodes so nothing upstream
89
89
  is recomputed.
90
- - **Low-level primitives** (`graph/primitives.py`) `load/diffusion-model`, `load/vae`,
90
+ - **Low-level primitives** (`graph/primitives.py`) - `load/diffusion-model`, `load/vae`,
91
91
  `load/text-encoder`, `encode/text`, `latent/empty`, `sample`, `vae/decode`, `vae/encode`. These are
92
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
93
+ today - their runners land in C2.** A graph built from them validates and type-checks but raises
94
94
  `No runner registered` at execution.
95
- - **Model runners** (`models/<name>/`) high-level single generation nodes. **`alibaba/z-image-turbo`
95
+ - **Model runners** (`models/<name>/`) - high-level single generation nodes. **`alibaba/z-image-turbo`
96
96
  (`models/zimage/`) is the one runnable generation path today** (prompt + optional image → one take,
97
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):
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
100
  the runner loads the transformer via `from_single_file` and pulls the VAE / text-encoder / tokenizer
101
101
  from the reference repo behind the scenes, so the user only ever handles one model file.
102
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
103
+ validation/execution but never offered in the UI's add-node menu. Generation stays one-click - the
104
104
  user sees only high-level model nodes; loaders/VAE/encoders are wired up behind them.
105
105
 
106
106
  Only media-output nodes (`vae/decode`, a model runner) become Frames with take history; the engine
@@ -109,26 +109,26 @@ between nodes and are never takes.
109
109
 
110
110
  ### Storage & configuration (all env, see `config.py`)
111
111
 
112
- - **Models root** `INLINE_MODELS_DIR`, else `./models`. **Bring your own weights; nothing is
112
+ - **Models root** - `INLINE_MODELS_DIR`, else `./models`. **Bring your own weights; nothing is
113
113
  downloaded.** ComfyUI-style category subfolders (`diffusion_models/`, `vae/`, `text_encoders/`,
114
114
  `loras/`, `controlnet/`, `checkpoints/`, `clip_vision/`, `upscale_models/`, `embeddings/`). The
115
115
  catalog scans this on start; a file dropped in bumps the registry version so clients refetch
116
116
  `/v1/models`. A model may be a single weight file or a folder (e.g. a diffusers snapshot or a
117
117
  sharded text encoder).
118
- - **Data dir** `INLINE_DATA_DIR`, else `./.inline`. Engine-owned working data: `runs.db` (durable
118
+ - **Data dir** - `INLINE_DATA_DIR`, else `./.inline`. Engine-owned working data: `runs.db` (durable
119
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
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
122
  diffusers dir, or a HF repo id for Z-Image). Auto-resolved from `diffusion_models/` when unset.
123
- - **Memory** by default prefer the GPU, and **auto-fit the model to it**. When a runner hands the
123
+ - **Memory** - by default prefer the GPU, and **auto-fit the model to it**. When a runner hands the
124
124
  policy the model's on-disk sizes (`DevicePolicy.set_footprint`, a `ModelFootprint`), the policy sizes
125
125
  the weights against total VRAM (minus an activation headroom) and picks the lightest plan that fits:
126
126
  full-precision **resident** → else int8 **resident** (torchao halves the transformer + text encoder,
127
- no CPU offload int8 + accelerate's `enable_model_cpu_offload` deadlock together) → else unquantized
127
+ no CPU offload - int8 + accelerate's `enable_model_cpu_offload` deadlock together) → else unquantized
128
128
  `SEQUENTIAL` submodule streaming. So a card that can't hold Z-Image full-precision (a T4)
129
129
  **auto-int8s with no flag**; `set_footprint(None)` / an unsizable whole-pipeline folder falls back to
130
130
  the coarse total-VRAM buckets. Capacity is TOTAL VRAM (a fixed device property), not live-free, so the
131
- plan and the pipeline cache key it feeds is stable across runs. The runner does a **pre-flight
131
+ plan - and the pipeline cache key it feeds - is stable across runs. The runner does a **pre-flight
132
132
  check** (`DevicePolicy.fit_estimate`): a model too big for VRAM+RAM fails with a clean node error
133
133
  before any load, instead of a host-RAM OOM-kill that would take the shared server down. Weights stream
134
134
  straight to the GPU on load (`device=`/`device_map` in `models/loaders.py`) so peak host RAM ≈ one
@@ -136,11 +136,11 @@ between nodes and are never takes.
136
136
  `_evict_stale`) rather than stacking. `webui.sh` always sets `PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True`
137
137
  to cut fragmentation OOMs. **Smart memory** (`INLINE_SMART_MEMORY=1`, `--smart-memory`) and
138
138
  `INLINE_ALLOW_CPU_OFFLOAD=1` remain as explicit overrides when no footprint is set.
139
- - **Compute dtype** bf16 on the GPU by default, but **fp16 on cards without bf16 acceleration**
140
- (Turing/Volta compute capability < 8.0, e.g. a T4): same footprint, but it uses the fp16 tensor
139
+ - **Compute dtype** - bf16 on the GPU by default, but **fp16 on cards without bf16 acceleration**
140
+ (Turing/Volta - compute capability < 8.0, e.g. a T4): same footprint, but it uses the fp16 tensor
141
141
  cores instead of bf16's slow path. The **VAE stays upcast** (bf16, or fp32 when the denoiser is
142
142
  fp16) because fp16 VAE decode can overflow to black/NaN images (`_compute_dtype` in `device/`).
143
- - **Multi-GPU** `INLINE_PARALLEL` (e.g. `pipefusion=2`, `pipefusion=2,ulysses=2`); degrees multiply
143
+ - **Multi-GPU** - `INLINE_PARALLEL` (e.g. `pipefusion=2`, `pipefusion=2,ulysses=2`); degrees multiply
144
144
  to the world size, which must equal the GPU count.
145
145
 
146
146
  ### The `/v1` API (the contract with Inline Studio)
@@ -153,17 +153,17 @@ between nodes and are never takes.
153
153
  - `GET /v1/models/{type}`, `GET /v1/takes/{id}`, `GET /v1/takes/{id}/bytes`, `POST /v1/assets`,
154
154
  `GET /v1/health`.
155
155
 
156
- Errors are `{error: {code, message, nodeId?}}` with the right HTTP status they never leak a raw
156
+ Errors are `{error: {code, message, nodeId?}}` with the right HTTP status - they never leak a raw
157
157
  traceback. The JSON shapes live in `server/serialize.py`.
158
158
 
159
159
  ## Multi-GPU (xDiT): split one image across GPUs
160
160
 
161
- One image's **denoise loop** (the expensive part) runs collectively across GPUs not "one image per
161
+ One image's **denoise loop** (the expensive part) runs collectively across GPUs - not "one image per
162
162
  GPU". It's done with xfuser in an isolated worker group (one process per GPU via `torchrun`, over
163
163
  local IPC) behind the `XFuserBatchedSampler` seam. Single-GPU/CPU runs take the in-process path and
164
164
  pay no overhead. Split method is chosen from the detected interconnect: **PipeFusion** (default, PCIe)
165
165
  or **Ulysses** (NVLink). The policy and IPC round-trip are in place and tested with a stub worker; the
166
- real xfuser denoise lands with the GPU-side runner (C2). Keep `sampling/batch.py` torch-free the
166
+ real xfuser denoise lands with the GPU-side runner (C2). Keep `sampling/batch.py` torch-free - the
167
167
  real codec that moves tensors lives with the model runner.
168
168
 
169
169
  ## Code standards (non-negotiable)
@@ -175,7 +175,7 @@ real codec that moves tensors lives with the model runner.
175
175
  (`graph/validate.py` + `port_satisfies`) rejects bad wiring at submit. New port kinds go in
176
176
  `schema.py`.
177
177
  - **Device policy owns placement.** Components and runners **never** self-assign a device/dtype/
178
- offload they call `ctx.policy.placement(role)` (`text_encoder`, `denoiser`, `vae`, …). This is the
178
+ offload - they call `ctx.policy.placement(role)` (`text_encoder`, `denoiser`, `vae`, …). This is the
179
179
  rule that keeps one graph portable across GPU / low-VRAM / CPU / multi-GPU. The policy **prefers the
180
180
  GPU**: a low-VRAM GPU keeps weights resident (tiling/slicing/int8 do the saving) and does not
181
181
  auto-offload to CPU (`placement.offload` defaults False; opt in with `INLINE_ALLOW_CPU_OFFLOAD`).
@@ -183,7 +183,9 @@ real codec that moves tensors lives with the model runner.
183
183
  `SampleJob` through the batched-sampler seam. The executor orchestrates; it does not sample.
184
184
  - **Immutable takes.** The `TakeStore` owns bytes/hash/uri; regenerating adds a take. Never overwrite.
185
185
  - **Engine deps are optional and import-guarded.** Heavy deps (torch, diffusers, xfuser) live in
186
- `[project.optional-dependencies]` extras (`zimage`, `server`, `parallel`, `dev`). Model-runner
186
+ `[project.optional-dependencies]` extras (`runtime`, `server`, `parallel`, `dev`). **`runtime` is
187
+ the single shared ML stack - a new model must reuse it, never declare its own torch/diffusers
188
+ block.** Model-runner
187
189
  subpackages import torch/diffusers at module top **on purpose**: an absent extra makes the import
188
190
  raise, and `server/bootstrap.py` skips that model best-effort so a core install still boots and
189
191
  serves source nodes. Never import a heavy dep at package top level outside a runner subpackage.
@@ -200,14 +202,14 @@ real codec that moves tensors lives with the model runner.
200
202
 
201
203
  ```
202
204
  uv venv # create ./.venv
203
- uv pip install -e ".[dev]" # engine + server + test tooling
204
- uv pip install -e ".[zimage]" # + torch, diffusers, transformers (real generation)
205
- uv pip install -e ".[parallel]" # + xfuser, for multi-GPU denoise (2+ GPUs)
205
+ uv pip install -e ".[server,dev]" # engine + server + test tooling
206
+ uv pip install -e ".[runtime]" # + torch, diffusers, transformers (real generation)
207
+ uv pip install -e ".[runtime,parallel]" # + xfuser, for multi-GPU denoise (2+ GPUs)
206
208
 
207
209
  ./webui.sh # run (loopback:8848); friendly flags → INLINE_* env
208
210
  ./webui.sh --listen --port 9000 # bind all interfaces
209
211
  ./webui.sh --lowvram # tight-VRAM profile
210
- ./webui.sh --install --extra zimage # set up ./.venv with the Z-Image runtime, then exit
212
+ ./webui.sh --install --extra runtime # set up ./.venv with the model runtime, then exit
211
213
  python -m inline_core.server # run the server directly (INLINE_HOST / INLINE_PORT)
212
214
 
213
215
  ruff check . # lint (zero warnings)
@@ -219,7 +221,7 @@ uv run pytest -q # tests (no GPU; model code is import-
219
221
  - **New model runner** → a subpackage `models/<name>/` with `runner.py` (a `NodeDescriptor` + a
220
222
  `NodeRunner` + `register_<name>(registry, store, policy)`) and an `__init__.py` re-exporting it;
221
223
  add a `try/except ImportError` block in `server/bootstrap.py`; add an optional-deps extra in
222
- `pyproject.toml`. Copy `models/zimage/` it's the reference.
224
+ `pyproject.toml`. Copy `models/zimage/` - it's the reference.
223
225
  - **New low-level primitive** → descriptor in `graph/primitives.py`; its runner lands with the C2 work
224
226
  (build a component in `components/`, wire it through `encode`/`sample`/`vae` and the sampler seam).
225
227
  - **New `/v1` route** → add it in `server/app.py`, shape the JSON in `server/serialize.py`, keep errors
@@ -0,0 +1,247 @@
1
+ Metadata-Version: 2.4
2
+ Name: inline-core
3
+ Version: 1.2.0
4
+ Summary: The generation engine behind Inline Studio.
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: numpy>=1.26
7
+ Requires-Dist: psutil>=5.9
8
+ Provides-Extra: all
9
+ Requires-Dist: accelerate>=0.30; extra == 'all'
10
+ Requires-Dist: diffusers>=0.36; extra == 'all'
11
+ Requires-Dist: fastapi>=0.110; extra == 'all'
12
+ Requires-Dist: huggingface-hub>=0.23; extra == 'all'
13
+ Requires-Dist: imageio-ffmpeg>=0.4; extra == 'all'
14
+ Requires-Dist: safetensors>=0.4; extra == 'all'
15
+ Requires-Dist: scipy>=1.11; extra == 'all'
16
+ Requires-Dist: torch>=2.2; extra == 'all'
17
+ Requires-Dist: torchao>=0.14; extra == 'all'
18
+ Requires-Dist: transformers>=4.44; extra == 'all'
19
+ Requires-Dist: uvicorn[standard]>=0.29; extra == 'all'
20
+ Provides-Extra: dev
21
+ Requires-Dist: httpx>=0.27; extra == 'dev'
22
+ Requires-Dist: pyright>=1.1; extra == 'dev'
23
+ Requires-Dist: pytest>=8; extra == 'dev'
24
+ Requires-Dist: ruff>=0.6; extra == 'dev'
25
+ Provides-Extra: parallel
26
+ Requires-Dist: nvidia-ml-py>=12; extra == 'parallel'
27
+ Requires-Dist: xfuser>=0.4; extra == 'parallel'
28
+ Provides-Extra: runtime
29
+ Requires-Dist: accelerate>=0.30; extra == 'runtime'
30
+ Requires-Dist: diffusers>=0.36; extra == 'runtime'
31
+ Requires-Dist: huggingface-hub>=0.23; extra == 'runtime'
32
+ Requires-Dist: safetensors>=0.4; extra == 'runtime'
33
+ Requires-Dist: scipy>=1.11; extra == 'runtime'
34
+ Requires-Dist: torch>=2.2; extra == 'runtime'
35
+ Requires-Dist: torchao>=0.14; extra == 'runtime'
36
+ Requires-Dist: transformers>=4.44; extra == 'runtime'
37
+ Provides-Extra: server
38
+ Requires-Dist: fastapi>=0.110; extra == 'server'
39
+ Requires-Dist: imageio-ffmpeg>=0.4; extra == 'server'
40
+ Requires-Dist: uvicorn[standard]>=0.29; extra == 'server'
41
+ Description-Content-Type: text/markdown
42
+
43
+ # Inline Core
44
+
45
+ The generation engine behind Inline. It takes a typed node graph (JSON) and returns immutable renders
46
+ ("takes"), running image and video models on macOS, Windows, and Linux, from CPU-only boxes and
47
+ low-VRAM laptops up to multi-GPU machines that split a single image's sampling across GPUs (via
48
+ xDiT). It is Inline Studio's built-in render backend.
49
+
50
+ First model: Z-Image (Alibaba Tongyi), a 6B rectified-flow diffusion transformer (and a model xDiT
51
+ already supports, so the multi-GPU split works on it from the start).
52
+
53
+ > Status: early, and running end to end against a stub engine. In place and tested: the graph engine,
54
+ > the typed `/v1` HTTP + websocket API (durable runs, streamed progress, coalescing), the model-dir
55
+ > scan, the device + memory policy (profiles, dtype, offload, int8), the low-level primitive node
56
+ > vocabulary, and a ComfyUI workflow importer. The Z-Image loader is written and validates on a GPU.
57
+ > Cross-request batching, single-image multi-GPU (an xDiT worker group behind the sampler seam, with
58
+ > the policy and IPC round-trip tested), and out-of-process custom nodes are built as seams but not
59
+ > yet running on real hardware.
60
+
61
+ ## Engine design
62
+
63
+ The two boundaries that matter most: graph orchestration is decoupled from GPU batching (graphs are
64
+ the unit of caching, the sampler is the unit of batching, and the multi-GPU split routes through that
65
+ same seam), and the device policy is the single owner of placement, so the same graph runs on a 4090,
66
+ a 6 GB laptop, pure CPU, or split across several GPUs, without touching the graph.
67
+
68
+ | | |
69
+ | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
70
+ | Graph vs GPU | orchestration (cheap, per request) is separate from a batched sampler that groups compatible jobs across requests |
71
+ | Schema | typed graph, named params, edges type-checked before the run (a bad graph is rejected at submit) |
72
+ | Devices | 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 |
73
+ | Multi-GPU | 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 |
74
+ | Custom nodes | run out of process, each pack in its own venv, behind a semver SDK |
75
+ | Interface | a headless `/v1` HTTP + websocket API; runs are durable and survive a restart |
76
+ | Outputs | immutable takes; regenerating adds a take, never overwrites |
77
+ | Models | drop-in `models/` layout (bring your own, no downloads); a typed catalog feeds versioned node descriptors the UI renders generically |
78
+
79
+ ## Install
80
+
81
+ Requires Python 3.11+ and [uv](https://docs.astral.sh/uv/).
82
+
83
+ ```
84
+ uv venv
85
+ uv pip install -e ".[server]" # engine + HTTP/websocket API
86
+ uv pip install -e ".[runtime]" # + torch, diffusers, transformers (for real generation)
87
+ uv pip install -e ".[runtime,parallel]" # + xfuser, to split one image across GPUs (2+ GPUs)
88
+ ```
89
+
90
+ ## Models
91
+
92
+ Drop files into the models dir (default `./models`, override with `INLINE_MODELS_DIR`), ComfyUI-style,
93
+ by category:
94
+
95
+ ```
96
+ models/
97
+ diffusion_models/ z_image_turbo_bf16.safetensors <- the one file you need for Z-Image
98
+ vae/ ae.safetensors <- optional (see below)
99
+ text_encoders/ qwen/ (an HF-format folder: config + tokenizer + weights) <- optional
100
+ loras/ controlnet/ checkpoints/ ...
101
+ ```
102
+
103
+ **Z-Image loads from a single diffusion `.safetensors`.** Drop that one ComfyUI-style file in
104
+ `diffusion_models/`; the runner loads the transformer via diffusers `from_single_file`. It also needs
105
+ a VAE + text-encoder + tokenizer, resolved from **local files** - `vae/*.safetensors` or a dir, and an
106
+ HF-format `text_encoders/` dir.
107
+
108
+ **Nothing is ever downloaded as a side effect of a render.** Every load runs `local_files_only=True`,
109
+ so a missing model is a clear, fast error, not a silent multi-GB fetch. Models arrive by exactly two
110
+ paths:
111
+
112
+ 1. **You place files** under `models/` (bring-your-own / fully offline); or
113
+ 2. **You download them explicitly** from the Z-Image node's model popup in the UI - it lists the
114
+ diffusion model, VAE, and text-encoder, shows which are missing, and downloads them **into
115
+ `models/`** (never the hidden HF cache) with visible progress.
116
+
117
+ Override the diffusion source with `INLINE_ZIMAGE_MODEL` (a file or a diffusers dir), and the supporting
118
+ components with `INLINE_ZIMAGE_VAE` / `INLINE_ZIMAGE_TEXT_ENCODER`. The engine scans the models dir on
119
+ start; a node's model pickers list what is present.
120
+
121
+ ## Nodes
122
+
123
+ `/v1/models` serves each node's descriptor (ports, params, file pickers), so the Inline Studio canvas
124
+ renders any node generically - adding a node type needs no UI release.
125
+
126
+ **High-level model nodes are what the user sees.** Generation is one-click: you drop a single
127
+ **Z-Image Turbo** node, wire a Prompt into it, and hit Run. The node hooks up the diffusion model, VAE,
128
+ and text-encoder behind the scenes - no loader/sampler wiring.
129
+
130
+ Underneath, a **low-level primitive vocabulary** exists (`load/diffusion-model`, `load/vae`,
131
+ `load/text-encoder`, `encode/text`, `latent/empty`, `sample`, `vae/decode`, `vae/encode`) - the
132
+ ComfyUI-equivalent decomposed graph, kept for validation/execution - but these are marked **`hidden`**
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. A
135
+ best-effort ComfyUI importer maps existing workflows onto the primitives.
136
+
137
+ ## Multi-GPU: split one image across GPUs
138
+
139
+ Cut a single image's latency by running its denoise across several GPUs. The denoise loop (the
140
+ iterative sampling step) is the expensive part of a render; with two or more GPUs, Inline Core runs
141
+ each step's transformer forward collectively across them so one image finishes faster. This is not
142
+ "one image per GPU" (independent renders); it is one image whose sampling is shared by all the GPUs.
143
+
144
+ It is done with xDiT (`xfuser`), which parallelizes diffusion-transformer inference. xfuser runs in
145
+ an isolated worker group the engine spawns (one process per GPU via `torchrun`) and talks to over
146
+ local IPC, so the HTTP server, database, and graph stay single-process and only the denoise is
147
+ distributed. It sits behind the sampler seam (`XFuserBatchedSampler`), so a single-GPU or CPU run
148
+ takes the in-process path and pays no overhead.
149
+
150
+ Two split methods, chosen from the interconnect the engine detects:
151
+
152
+ - **PipeFusion (default, PCIe):** shards the transformer into a displaced patch pipeline with low,
153
+ depth-independent communication. It needs no NVLink and works over plain PCIe (or Ethernet across
154
+ nodes), so it is the default on a typical multi-GPU box.
155
+ - **Ulysses (NVLink):** sequence-parallel attention, used when NVLink is present because it wants
156
+ the higher interconnect bandwidth.
157
+
158
+ Enabling it:
159
+
160
+ 1. **Install the extra** and have 2+ CUDA GPUs on one machine:
161
+ ```
162
+ uv pip install -e ".[runtime,parallel]" # xfuser + nvidia-ml-py; torchrun ships with torch
163
+ ```
164
+ 2. **Run normally.** On the first denoise, the device policy enumerates the GPUs, detects NVLink vs
165
+ PCIe (via `nvidia-ml-py`), and returns a parallel placement when there is more than one GPU. The
166
+ engine then spawns the xfuser worker group (lazily, once, then reuses it) and splits the sampling
167
+ across the GPUs. No graph, API, or per-request change is needed.
168
+ 3. **Override the split** if you want to pick it by hand, with `INLINE_PARALLEL`:
169
+ ```
170
+ INLINE_PARALLEL=pipefusion=2 # 2 GPUs, PipeFusion
171
+ INLINE_PARALLEL=pipefusion=2,ulysses=2 # 4 GPUs, PipeFusion x Ulysses
172
+ ```
173
+ The degrees multiply to the world size, which must equal the number of GPUs.
174
+
175
+ The device policy and the worker-group IPC are in place and tested with a stub worker; the real
176
+ xfuser denoise lands with the GPU-side Z-Image runner.
177
+
178
+ ## Memory: fit a big model onto a small GPU
179
+
180
+ When a model is too large to hold full-precision on your GPU - or larger than your system RAM - Inline
181
+ Core fits it to the machine automatically, **with no flags**. This is the low-VRAM path, and it aims to
182
+ "just run" instead of making you tune profiles.
183
+
184
+ - **Weights stream straight to the GPU.** Each component loads directly onto the GPU from its
185
+ memory-mapped `.safetensors` - the engine never materializes a full copy in system RAM first. So you
186
+ do **not** need roughly 2× the model size in RAM to load it, and a load can no longer exhaust host RAM
187
+ and take the server down.
188
+ - **Auto-fit picks the lightest plan that fits.** The policy sizes the model against your VRAM and
189
+ chooses, in order: full-precision **resident** → **int8 resident** (weight-only quantization halves the
190
+ big weights so they fit on the GPU) → **CPU-offload streaming** (submodules move on/off the GPU). A card
191
+ that can't hold the model full-precision quantizes to int8 on its own - no need to know a flag.
192
+ - **Load-time fit check.** A model too large for VRAM + RAM fails fast with a clear message on the node,
193
+ instead of crashing mid-load. The model popup also shows the estimate up front (fits / will run int8 /
194
+ won't fit).
195
+ - **Switching models frees the old one.** Loading a different checkpoint evicts the previous weights
196
+ rather than stacking them, so you can move between models without accumulating VRAM.
197
+ - **Generation fits too, not just loading.** Prompts encode under `no_grad` so the text-encoder forward
198
+ never retains its activation graph, and the encoder is parked on the CPU during denoise to free its
199
+ VRAM; large images (1024² and up) decode the VAE in tiles instead of one full-frame pass. So the
200
+ memory-heavy steps - encode and decode - stay bounded rather than spiking host RAM or VRAM and taking
201
+ the server down at high resolution.
202
+ - **Fragmentation-resistant allocator.** `webui.sh` runs with PyTorch expandable CUDA segments, which
203
+ avoids the "allocation failed with VRAM still free" fragmentation OOMs common on low-VRAM cards.
204
+
205
+ You can still steer it by hand: `--lowvram` for the tight-VRAM profile, `--smart-memory` to force the
206
+ offload machinery, `--vram-budget GB` / `INLINE_VRAM_BUDGET_GB` to cap the assumed VRAM, or
207
+ `INLINE_PROFILE` to pin `gpu-max | lowvram | cpu`. `GET /v1/health` reports the live VRAM/RAM budget.
208
+
209
+ ## Run
210
+
211
+ The easy path is `webui.sh`, which maps friendly flags onto the engine's `INLINE_*` env knobs:
212
+
213
+ ```
214
+ ./webui.sh # loopback, port 8848
215
+ ./webui.sh --listen --port 9000 # bind all interfaces on 9000
216
+ ./webui.sh --multi-gpu # split one image across GPUs (auto with 2+ GPUs)
217
+ ./webui.sh --lowvram # tight-VRAM profile
218
+ ./webui.sh --install --extra zimage # set up ./.venv with the Z-Image runtime, then exit
219
+ ```
220
+
221
+ `./webui.sh --help` lists every flag (networking, multi-GPU, device/memory profile, paths). The same
222
+ flags are available on the Python entrypoint - `python main.py --help` - which is the dev path (it also
223
+ takes `--front-end-root DIR` to serve a local SPA build). Or run the server module directly:
224
+
225
+ ```
226
+ python main.py --listen --port 9000 # same flags as webui.sh; --front-end-root for a local UI build
227
+ python -m inline_core.server # bare server (INLINE_HOST / INLINE_PORT from the env)
228
+ ```
229
+
230
+ Working data (the run database and takes) lives in `INLINE_DATA_DIR` (default `./.inline`).
231
+
232
+ ## API (v1)
233
+
234
+ - `POST /v1/runs {graph, target}` returns `{runId}` (validated up front; 422 on a bad graph)
235
+ - `GET /v1/runs/{id}` returns run state (durable; survives a restart)
236
+ - `GET /v1/runs/{id}/events` (websocket): a snapshot, then `progress` / `node_done` / `run_done`
237
+ - `DELETE /v1/runs/{id}` cancels
238
+ - `GET /v1/models` returns node descriptors + `registryVersion` (ETag-aware)
239
+ - `GET /v1/takes/{id}` and `/v1/takes/{id}/bytes`
240
+ - `POST /v1/assets` (content-addressed upload) and `GET /v1/health`
241
+
242
+ ## Development
243
+
244
+ ```
245
+ ruff check . # lint
246
+ uv run pytest -q # tests (no GPU needed; model code is import-guarded)
247
+ ```