inline-core 1.2.5__tar.gz → 1.2.6__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 (211) hide show
  1. {inline_core-1.2.5 → inline_core-1.2.6}/CLAUDE.md +33 -4
  2. {inline_core-1.2.5 → inline_core-1.2.6}/PKG-INFO +24 -7
  3. {inline_core-1.2.5 → inline_core-1.2.6}/README.md +12 -6
  4. {inline_core-1.2.5 → inline_core-1.2.6}/pyproject.toml +24 -3
  5. inline_core-1.2.6/src/inline_core/__init__.py +14 -0
  6. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/device/memory.py +16 -4
  7. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/device/policy.py +5 -1
  8. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/graph/cache.py +35 -2
  9. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/graph/executor.py +16 -4
  10. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/graph/schema.py +5 -1
  11. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/models/catalog.py +21 -0
  12. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/models/checkpoint.py +5 -0
  13. inline_core-1.2.6/src/inline_core/models/controlspace.py +26 -0
  14. inline_core-1.2.6/src/inline_core/models/flux2/__init__.py +1 -0
  15. inline_core-1.2.6/src/inline_core/models/flux2/controlnet.py +234 -0
  16. inline_core-1.2.6/src/inline_core/models/flux2/embeds.py +165 -0
  17. inline_core-1.2.6/src/inline_core/models/flux2/provider.py +84 -0
  18. inline_core-1.2.6/src/inline_core/models/flux2/requirements.py +414 -0
  19. inline_core-1.2.6/src/inline_core/models/flux2/runner.py +677 -0
  20. inline_core-1.2.6/src/inline_core/models/flux2/variants.py +334 -0
  21. inline_core-1.2.6/src/inline_core/models/krea2/depth_control.py +149 -0
  22. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/models/krea2/img2img.py +2 -2
  23. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/models/krea2/provider.py +11 -0
  24. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/models/krea2/requirements.py +75 -1
  25. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/models/krea2/runner.py +65 -6
  26. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/models/loaders.py +574 -7
  27. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/models/pipeline_runtime.py +60 -19
  28. inline_core-1.2.6/src/inline_core/models/preprocess/__init__.py +5 -0
  29. inline_core-1.2.6/src/inline_core/models/preprocess/requirements.py +59 -0
  30. inline_core-1.2.6/src/inline_core/models/preprocess/runner.py +174 -0
  31. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/models/requirements.py +47 -0
  32. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/models/zimage/provider.py +12 -0
  33. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/models/zimage/requirements.py +90 -4
  34. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/models/zimage/runner.py +72 -8
  35. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/server/app.py +30 -4
  36. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/server/bootstrap.py +25 -0
  37. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/server/serialize.py +28 -4
  38. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/studio/fal.py +43 -5
  39. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/studio/frames.py +44 -18
  40. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/studio/generation.py +38 -8
  41. inline_core-1.2.6/src/inline_core/studio/graph_build.py +326 -0
  42. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/studio/handlers.py +48 -9
  43. inline_core-1.2.6/src/inline_core/studio/image_meta.py +36 -0
  44. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/studio/models.py +6 -2
  45. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/studio/moodboard.py +52 -3
  46. inline_core-1.2.6/src/inline_core/studio/recipe.py +109 -0
  47. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/studio/schema.py +8 -2
  48. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/studio/training.py +10 -2
  49. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/training/arch.py +107 -4
  50. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/training/caption.py +39 -6
  51. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/training/dataset.py +54 -3
  52. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/training/models.py +132 -2
  53. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/training/trainer.py +28 -3
  54. {inline_core-1.2.5 → inline_core-1.2.6}/tests/helpers.py +1 -0
  55. inline_core-1.2.6/tests/test_cache.py +116 -0
  56. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_executor.py +26 -1
  57. inline_core-1.2.6/tests/test_flux2_controlnet.py +159 -0
  58. inline_core-1.2.6/tests/test_flux2_folder.py +179 -0
  59. inline_core-1.2.6/tests/test_flux2_resolve.py +176 -0
  60. inline_core-1.2.6/tests/test_flux2_runner.py +126 -0
  61. inline_core-1.2.6/tests/test_flux2_training.py +199 -0
  62. inline_core-1.2.6/tests/test_flux2_variants.py +129 -0
  63. inline_core-1.2.6/tests/test_krea2_depth_control.py +104 -0
  64. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_krea2_requirements.py +35 -2
  65. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_krea2_runner.py +4 -3
  66. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_loaders.py +35 -0
  67. inline_core-1.2.6/tests/test_lora_download.py +75 -0
  68. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_memory_policy.py +39 -0
  69. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_model_requirements.py +25 -0
  70. inline_core-1.2.6/tests/test_pipeline_cache.py +82 -0
  71. inline_core-1.2.6/tests/test_recipe.py +100 -0
  72. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_studio_fal.py +46 -0
  73. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_studio_generation.py +41 -3
  74. inline_core-1.2.6/tests/test_studio_graph_build.py +111 -0
  75. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_studio_models.py +20 -0
  76. inline_core-1.2.6/tests/test_studio_multi_reference.py +163 -0
  77. inline_core-1.2.6/tests/test_studio_node_size.py +58 -0
  78. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_studio_rpc.py +13 -0
  79. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_studio_schema.py +19 -0
  80. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_training_models.py +29 -0
  81. inline_core-1.2.6/tests/test_webui_install.py +154 -0
  82. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_zimage_resolve.py +6 -2
  83. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_zimage_runner.py +109 -13
  84. {inline_core-1.2.5 → inline_core-1.2.6}/uv.lock +258 -1
  85. inline_core-1.2.6/webui.bat +319 -0
  86. {inline_core-1.2.5 → inline_core-1.2.6}/webui.sh +96 -18
  87. inline_core-1.2.5/src/inline_core/__init__.py +0 -7
  88. inline_core-1.2.5/src/inline_core/studio/graph_build.py +0 -141
  89. inline_core-1.2.5/tests/test_cache.py +0 -48
  90. {inline_core-1.2.5 → inline_core-1.2.6}/.gitignore +0 -0
  91. {inline_core-1.2.5 → inline_core-1.2.6}/.python-version +0 -0
  92. {inline_core-1.2.5 → inline_core-1.2.6}/main.py +0 -0
  93. {inline_core-1.2.5 → inline_core-1.2.6}/scripts/reference.py +0 -0
  94. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/components/__init__.py +0 -0
  95. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/components/conditioning.py +0 -0
  96. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/components/interfaces.py +0 -0
  97. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/config.py +0 -0
  98. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/device/__init__.py +0 -0
  99. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/device/auto.py +0 -0
  100. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/device/detect.py +0 -0
  101. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/device/types.py +0 -0
  102. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/errors.py +0 -0
  103. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/extensions/__init__.py +0 -0
  104. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/extensions/api.py +0 -0
  105. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/extensions/constraints.py +0 -0
  106. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/extensions/fetch.py +0 -0
  107. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/extensions/handlers.py +0 -0
  108. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/extensions/importer.py +0 -0
  109. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/extensions/install.py +0 -0
  110. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/extensions/loader.py +0 -0
  111. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/extensions/manifest.py +0 -0
  112. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/extensions/models.py +0 -0
  113. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/extensions/paths.py +0 -0
  114. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/extensions/resolve.py +0 -0
  115. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/extensions/scanner.py +0 -0
  116. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/extensions/state.py +0 -0
  117. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/extensions/tools.py +0 -0
  118. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/ffmpeg.py +0 -0
  119. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/graph/__init__.py +0 -0
  120. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/graph/descriptor.py +0 -0
  121. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/graph/loader_runners.py +0 -0
  122. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/graph/primitives.py +0 -0
  123. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/graph/registry.py +0 -0
  124. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/graph/runners.py +0 -0
  125. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/graph/topo.py +0 -0
  126. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/graph/validate.py +0 -0
  127. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/media.py +0 -0
  128. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/models/__init__.py +0 -0
  129. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/models/krea2/__init__.py +0 -0
  130. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/models/krea2/convert.py +0 -0
  131. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/models/lora.py +0 -0
  132. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/models/sampling.py +0 -0
  133. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/models/zimage/__init__.py +0 -0
  134. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/models/zimage/primitives.py +0 -0
  135. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/parallel/__init__.py +0 -0
  136. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/parallel/config.py +0 -0
  137. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/parallel/group.py +0 -0
  138. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/parallel/launch.py +0 -0
  139. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/parallel/protocol.py +0 -0
  140. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/parallel/registry.py +0 -0
  141. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/parallel/worker.py +0 -0
  142. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/runtime/__init__.py +0 -0
  143. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/runtime/context.py +0 -0
  144. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/runtime/file_store.py +0 -0
  145. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/runtime/progress.py +0 -0
  146. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/runtime/run.py +0 -0
  147. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/runtime/store.py +0 -0
  148. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/sampling/__init__.py +0 -0
  149. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/sampling/batch.py +0 -0
  150. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/server/__init__.py +0 -0
  151. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/server/__main__.py +0 -0
  152. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/server/assets.py +0 -0
  153. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/server/frontend.py +0 -0
  154. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/server/manager.py +0 -0
  155. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/server/rpc.py +0 -0
  156. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/server/run_store.py +0 -0
  157. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/studio/__init__.py +0 -0
  158. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/studio/assets.py +0 -0
  159. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/studio/config.py +0 -0
  160. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/studio/peaks.py +0 -0
  161. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/studio/store.py +0 -0
  162. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/studio/system_stats.py +0 -0
  163. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/studio/timeline/__init__.py +0 -0
  164. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/studio/timeline/compose.py +0 -0
  165. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/studio/timeline/ffmpeg.py +0 -0
  166. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/studio/timeline/render.py +0 -0
  167. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/studio/timeline/resolve.py +0 -0
  168. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/studio/training_store.py +0 -0
  169. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/takes.py +0 -0
  170. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/training/__init__.py +0 -0
  171. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/training/__main__.py +0 -0
  172. {inline_core-1.2.5 → inline_core-1.2.6}/src/inline_core/training/protocol.py +0 -0
  173. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_catalog.py +0 -0
  174. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_checkpoint.py +0 -0
  175. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_config.py +0 -0
  176. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_device_detect.py +0 -0
  177. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_extension_api.py +0 -0
  178. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_extension_install.py +0 -0
  179. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_extension_manifest.py +0 -0
  180. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_extension_resolve.py +0 -0
  181. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_extension_scanner.py +0 -0
  182. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_extension_spine.py +0 -0
  183. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_extension_state.py +0 -0
  184. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_file_store.py +0 -0
  185. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_frontend_serving.py +0 -0
  186. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_hidden_nodes.py +0 -0
  187. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_krea2_convert.py +0 -0
  188. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_loader_runners.py +0 -0
  189. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_lora.py +0 -0
  190. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_parallel_group.py +0 -0
  191. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_primitives.py +0 -0
  192. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_rpc_bridge.py +0 -0
  193. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_run_store.py +0 -0
  194. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_sampling.py +0 -0
  195. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_schema.py +0 -0
  196. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_server.py +0 -0
  197. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_studio_assets.py +0 -0
  198. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_studio_frames.py +0 -0
  199. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_studio_moodboard.py +0 -0
  200. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_studio_peaks.py +0 -0
  201. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_studio_store.py +0 -0
  202. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_studio_timeline.py +0 -0
  203. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_studio_training.py +0 -0
  204. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_take_bytes.py +0 -0
  205. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_topo.py +0 -0
  206. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_training_arch.py +0 -0
  207. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_training_dataset.py +0 -0
  208. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_training_resolve.py +0 -0
  209. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_validate.py +0 -0
  210. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_xfuser_sampler.py +0 -0
  211. {inline_core-1.2.5 → inline_core-1.2.6}/tests/test_zimage_primitives.py +0 -0
@@ -95,7 +95,16 @@ HTTP/WS → server/app.py → RunManager → Executor → Registry (desc
95
95
  the ComfyUI-equivalent decomposed graph and the intended long-term surface. **Descriptor-only
96
96
  today - their runners land in C2.** A graph built from them validates and type-checks but raises
97
97
  `No runner registered` at execution.
98
- - **Model runners** (`models/<name>/`) - high-level single generation nodes. **`alibaba/z-image-turbo`
98
+ - **Model runners** (`models/<name>/`) - high-level single generation nodes. **`black-forest-labs/flux-2`
99
+ (`models/flux2/`) is the reference for a _family_:** one node covers klein 4B/9B, their base builds,
100
+ the KV variant and dev, because the picked checkpoint is identified from its own tensor shapes
101
+ (`flux2/variants.py`) and everything else follows. Adding a checkpoint is a row in that table.
102
+ Two rules it establishes: **identify a checkpoint by content, never by filename** (`diffusion_models/`
103
+ is shared across architectures, and Qwen3-4B and Qwen3-VL-4B have byte-identical embedding shapes),
104
+ and **derive geometry from the checkpoint** rather than bundling a config per variant, so a future
105
+ build loads with no code change. A **diffusers folder** is a valid checkpoint too, which is the only
106
+ way a 32B model reaches a 24 GB card: its NF4 shards stream through `from_pretrained`, where
107
+ `from_single_file` cannot quantize at all. **`alibaba/z-image-turbo`
99
108
  (`models/zimage/`) is the one runnable generation path today** (prompt + optional image → one take,
100
109
  backed by diffusers' `ZImagePipeline`/`ZImageImg2ImgPipeline`). This is the "Z-Image pipeline that
101
110
  already works" - the primitives will reach parity in C2. It loads from a **single diffusion
@@ -143,6 +152,18 @@ between nodes and are never takes.
143
152
  (Turing/Volta - compute capability < 8.0, e.g. a T4): same footprint, but it uses the fp16 tensor
144
153
  cores instead of bf16's slow path. The **VAE stays upcast** (bf16, or fp32 when the denoiser is
145
154
  fp16) because fp16 VAE decode can overflow to black/NaN images (`_compute_dtype` in `device/`).
155
+ - **Quantization rungs** - the fit ladder is full-precision resident → int8 resident → **NF4 resident**
156
+ → sequential offload → wont-fit. NF4 is what makes a 32B model viable on a 24 GB card. Note int8
157
+ forces bf16 (torchao's weight-only int8 silently no-ops under fp16) while NF4 does not, so a Turing
158
+ card keeps its fp16 tensor cores under NF4.
159
+ - **Prequantized checkpoints must not be re-quantized.** A checkpoint that ships already quantized
160
+ (`flux2/variants.is_prequantized`) has an on-disk size that already _is_ its resident size, so the
161
+ ladder's assumption that quantization halves it does not hold, and handing diffusers a second,
162
+ different quantization config is a hard error. Pass `Quantization.NONE` for those.
163
+ - **Staged loading** - when the text encoder and the transformer cannot be co-resident (dev is a
164
+ 15 GB encoder beside an 18 GB transformer), the prompt is encoded first and the encoder freed before
165
+ the transformer loads. The decision has to be made _before_ the load: by the time an OOM fires there
166
+ is nothing left to free.
146
167
  - **Multi-GPU** - `INLINE_PARALLEL` (e.g. `pipefusion=2`, `pipefusion=2,ulysses=2`); degrees multiply
147
168
  to the world size, which must equal the GPU count.
148
169
 
@@ -202,6 +223,11 @@ real codec that moves tensors lives with the model runner.
202
223
  Don't scatter it.
203
224
  - **Bring-your-own models.** Nothing is downloaded by the engine. The catalog scans; the user places
204
225
  files. A model picker is a `SELECT` param with `options_from="<category>"`.
226
+ - **Verify image models by rendering.** The FLUX.2 work shipped five bugs past a green test suite,
227
+ and every one produced a _wrong image rather than an error_: a mis-keyed checkpoint, a
228
+ vision-language encoder loaded in place of a text one, an unnormalized latent, and a control context
229
+ cast to a quantized weight's `uint8` storage dtype. A unit test cannot see a plausible-but-wrong
230
+ image. Render something and look at it.
205
231
  - **Tests (pytest).** Cover the logic that matters: graph validate/topo/executor/cache, the catalog
206
232
  scan, the run store + server contract, the device/memory policy, the parallel group + xfuser seam,
207
233
  and each model runner (import-guarded, no GPU needed). See `tests/`.
@@ -211,14 +237,17 @@ real codec that moves tensors lives with the model runner.
211
237
 
212
238
  ```
213
239
  uv venv # create ./.venv
214
- uv pip install -e ".[server,dev]" # engine + server + test tooling
215
- uv pip install -e ".[runtime]" # + torch, diffusers, transformers (real generation)
216
- uv pip install -e ".[runtime,parallel]" # + xfuser, for multi-GPU denoise (2+ GPUs)
240
+ # --python pins the target: without it uv installs into an activated venv/conda env, not ./.venv
241
+ uv pip install --python .venv/bin/python -e ".[server,dev]" # engine + server + test tooling
242
+ uv pip install --python .venv/bin/python -e ".[runtime]" # + torch, diffusers, transformers
243
+ uv pip install --python .venv/bin/python -e ".[runtime,parallel]" # + xfuser, multi-GPU denoise
217
244
 
218
245
  ./webui.sh # run (loopback:8848); friendly flags → INLINE_* env
219
246
  ./webui.sh --listen --port 9000 # bind all interfaces
220
247
  ./webui.sh --lowvram # tight-VRAM profile
221
248
  ./webui.sh --install --extra runtime # set up ./.venv with the model runtime, then exit
249
+ # (reuses an existing ./.venv; --recreate rebuilds it, and
250
+ # an activated foreign env is reported, never modified)
222
251
  python -m inline_core.server # run the server directly (INLINE_HOST / INLINE_PORT)
223
252
 
224
253
  ruff check . # lint (zero warnings)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: inline-core
3
- Version: 1.2.5
3
+ Version: 1.2.6
4
4
  Summary: The generation engine behind Inline Studio.
5
5
  License-Expression: GPL-3.0-or-later
6
6
  Requires-Python: >=3.11
@@ -8,12 +8,21 @@ Requires-Dist: numpy>=1.26
8
8
  Requires-Dist: psutil>=5.9
9
9
  Provides-Extra: all
10
10
  Requires-Dist: accelerate>=0.30; extra == 'all'
11
+ Requires-Dist: bitsandbytes>=0.43; (platform_system != 'Darwin') and extra == 'all'
12
+ Requires-Dist: controlnet-aux>=0.0.7; extra == 'all'
11
13
  Requires-Dist: diffusers>=0.39; extra == 'all'
14
+ Requires-Dist: einops>=0.7; extra == 'all'
12
15
  Requires-Dist: fastapi>=0.110; extra == 'all'
13
16
  Requires-Dist: huggingface-hub>=0.23; extra == 'all'
14
17
  Requires-Dist: imageio-ffmpeg>=0.4; extra == 'all'
18
+ Requires-Dist: nvidia-ml-py>=12; extra == 'all'
19
+ Requires-Dist: onnxruntime>=1.17; extra == 'all'
20
+ Requires-Dist: peft>=0.11; extra == 'all'
21
+ Requires-Dist: pillow>=10; extra == 'all'
22
+ Requires-Dist: psutil>=5.9; extra == 'all'
15
23
  Requires-Dist: safetensors>=0.4; extra == 'all'
16
24
  Requires-Dist: scipy>=1.11; extra == 'all'
25
+ Requires-Dist: timm>=1.0; extra == 'all'
17
26
  Requires-Dist: torch>=2.2; extra == 'all'
18
27
  Requires-Dist: torchao>=0.14; extra == 'all'
19
28
  Requires-Dist: transformers>=4.44; extra == 'all'
@@ -28,8 +37,10 @@ Requires-Dist: nvidia-ml-py>=12; extra == 'parallel'
28
37
  Requires-Dist: xfuser>=0.4; extra == 'parallel'
29
38
  Provides-Extra: runtime
30
39
  Requires-Dist: accelerate>=0.30; extra == 'runtime'
40
+ Requires-Dist: controlnet-aux>=0.0.7; extra == 'runtime'
31
41
  Requires-Dist: diffusers>=0.39; extra == 'runtime'
32
42
  Requires-Dist: huggingface-hub>=0.23; extra == 'runtime'
43
+ Requires-Dist: onnxruntime>=1.17; extra == 'runtime'
33
44
  Requires-Dist: safetensors>=0.4; extra == 'runtime'
34
45
  Requires-Dist: scipy>=1.11; extra == 'runtime'
35
46
  Requires-Dist: torch>=2.2; extra == 'runtime'
@@ -90,13 +101,18 @@ a 6 GB laptop, pure CPU, or split across several GPUs, without touching the grap
90
101
 
91
102
  Requires Python 3.11+ and [uv](https://docs.astral.sh/uv/).
92
103
 
104
+ `--python` pins each install to `./.venv`; without it uv targets whatever venv or conda env is
105
+ activated in your shell, and installs land there instead.
106
+
93
107
  ```
94
108
  uv venv
95
- uv pip install -e ".[server]" # engine + HTTP/websocket API
96
- uv pip install -e ".[runtime]" # + torch, diffusers, transformers (for real generation)
97
- uv pip install -e ".[runtime,parallel]" # + xfuser, to split one image across GPUs (2+ GPUs)
109
+ uv pip install --python .venv/bin/python -e ".[server]" # engine + HTTP/websocket API
110
+ uv pip install --python .venv/bin/python -e ".[runtime]" # + torch, diffusers, transformers
111
+ uv pip install --python .venv/bin/python -e ".[runtime,parallel]" # + xfuser, 2+ GPUs
98
112
  ```
99
113
 
114
+ Or let the launcher do it: `./webui.sh --install --extra runtime` (Windows: `.\webui.bat`).
115
+
100
116
  ## Models
101
117
 
102
118
  Drop files into the models dir (default `./models`, override with `INLINE_MODELS_DIR`), ComfyUI-style,
@@ -231,17 +247,18 @@ offload machinery, `--vram-budget GB` / `INLINE_VRAM_BUDGET_GB` to cap the assum
231
247
 
232
248
  ## Run
233
249
 
234
- The easy path is `webui.sh`, which maps friendly flags onto the engine's `INLINE_*` env knobs:
250
+ The easy path is `webui.sh` (macOS/Linux) or `webui.bat` (Windows), which maps friendly flags onto the
251
+ engine's `INLINE_*` env knobs. Windows must use `webui.bat` - `webui.sh` is a bash script:
235
252
 
236
253
  ```
237
- ./webui.sh # loopback, port 8848
254
+ ./webui.sh # loopback, port 8848 (Windows: .\webui.bat)
238
255
  ./webui.sh --listen --port 9000 # bind all interfaces on 9000
239
256
  ./webui.sh --multi-gpu # split one image across GPUs (auto with 2+ GPUs)
240
257
  ./webui.sh --lowvram # tight-VRAM profile
241
258
  ./webui.sh --install --extra runtime # set up ./.venv with the model runtime, then exit
242
259
  ```
243
260
 
244
- `./webui.sh --help` lists every flag (networking, multi-GPU, device/memory profile, paths). The same
261
+ `./webui.sh --help` (or `.\webui.bat --help`) lists every flag (networking, multi-GPU, device/memory profile, paths). The same
245
262
  flags are available on the Python entrypoint - `python main.py --help` - which is the dev path (it also
246
263
  takes `--front-end-root DIR` to serve a local SPA build). Or run the server module directly:
247
264
 
@@ -39,13 +39,18 @@ a 6 GB laptop, pure CPU, or split across several GPUs, without touching the grap
39
39
 
40
40
  Requires Python 3.11+ and [uv](https://docs.astral.sh/uv/).
41
41
 
42
+ `--python` pins each install to `./.venv`; without it uv targets whatever venv or conda env is
43
+ activated in your shell, and installs land there instead.
44
+
42
45
  ```
43
46
  uv venv
44
- uv pip install -e ".[server]" # engine + HTTP/websocket API
45
- uv pip install -e ".[runtime]" # + torch, diffusers, transformers (for real generation)
46
- uv pip install -e ".[runtime,parallel]" # + xfuser, to split one image across GPUs (2+ GPUs)
47
+ uv pip install --python .venv/bin/python -e ".[server]" # engine + HTTP/websocket API
48
+ uv pip install --python .venv/bin/python -e ".[runtime]" # + torch, diffusers, transformers
49
+ uv pip install --python .venv/bin/python -e ".[runtime,parallel]" # + xfuser, 2+ GPUs
47
50
  ```
48
51
 
52
+ Or let the launcher do it: `./webui.sh --install --extra runtime` (Windows: `.\webui.bat`).
53
+
49
54
  ## Models
50
55
 
51
56
  Drop files into the models dir (default `./models`, override with `INLINE_MODELS_DIR`), ComfyUI-style,
@@ -180,17 +185,18 @@ offload machinery, `--vram-budget GB` / `INLINE_VRAM_BUDGET_GB` to cap the assum
180
185
 
181
186
  ## Run
182
187
 
183
- The easy path is `webui.sh`, which maps friendly flags onto the engine's `INLINE_*` env knobs:
188
+ The easy path is `webui.sh` (macOS/Linux) or `webui.bat` (Windows), which maps friendly flags onto the
189
+ engine's `INLINE_*` env knobs. Windows must use `webui.bat` - `webui.sh` is a bash script:
184
190
 
185
191
  ```
186
- ./webui.sh # loopback, port 8848
192
+ ./webui.sh # loopback, port 8848 (Windows: .\webui.bat)
187
193
  ./webui.sh --listen --port 9000 # bind all interfaces on 9000
188
194
  ./webui.sh --multi-gpu # split one image across GPUs (auto with 2+ GPUs)
189
195
  ./webui.sh --lowvram # tight-VRAM profile
190
196
  ./webui.sh --install --extra runtime # set up ./.venv with the model runtime, then exit
191
197
  ```
192
198
 
193
- `./webui.sh --help` lists every flag (networking, multi-GPU, device/memory profile, paths). The same
199
+ `./webui.sh --help` (or `.\webui.bat --help`) lists every flag (networking, multi-GPU, device/memory profile, paths). The same
194
200
  flags are available on the Python entrypoint - `python main.py --help` - which is the dev path (it also
195
201
  takes `--front-end-root DIR` to serve a local SPA build). Or run the server module directly:
196
202
 
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  # PyPI name; the import package is `inline_core` (src/inline_core).
3
3
  name = "inline-core"
4
- version = "1.2.5"
4
+ version = "1.2.6"
5
5
  description = "The generation engine behind Inline Studio."
6
6
  readme = "README.md"
7
7
  license = "GPL-3.0-or-later"
@@ -28,6 +28,10 @@ runtime = [
28
28
  "scipy>=1.11",
29
29
  # We call snapshot_download directly for the model popup, so pin it rather than rely on transit.
30
30
  "huggingface_hub>=0.23",
31
+ # ControlNet preprocessors (the Apply ControlNet node): OpenPose/DWPose, MiDaS/Zoe depth, canny,
32
+ # HED, lineart, MLSD, scribble, normal. DWPose runs its detector on ONNX Runtime.
33
+ "controlnet-aux>=0.0.7",
34
+ "onnxruntime>=1.17",
31
35
  ]
32
36
  server = [
33
37
  "fastapi>=0.110",
@@ -60,8 +64,12 @@ dev = [
60
64
  "pyright>=1.1",
61
65
  "httpx>=0.27",
62
66
  ]
63
- # Everything needed to run locally. Lists concrete packages, never `inline-core[...]` self-refs.
67
+ # Everything a local install needs in one extra: runtime + server + training (the LoRA trainer),
68
+ # so `--install --extra all` is a single "install everything" command. Multi-GPU (xfuser, the
69
+ # `parallel` extra) is left out on purpose - it is niche and can fail to build on many systems.
70
+ # Lists concrete packages, never `inline-core[...]` self-refs.
64
71
  all = [
72
+ # runtime
65
73
  "torch>=2.2",
66
74
  "diffusers>=0.39",
67
75
  "transformers>=4.44",
@@ -70,9 +78,20 @@ all = [
70
78
  "torchao>=0.14",
71
79
  "scipy>=1.11",
72
80
  "huggingface_hub>=0.23",
81
+ "controlnet-aux>=0.0.7",
82
+ "onnxruntime>=1.17",
83
+ # server
73
84
  "fastapi>=0.110",
74
85
  "uvicorn[standard]>=0.29",
75
86
  "imageio-ffmpeg>=0.4",
87
+ # training (the Trainer tab)
88
+ "peft>=0.11",
89
+ "bitsandbytes>=0.43; platform_system != 'Darwin'",
90
+ "Pillow>=10",
91
+ "psutil>=5.9",
92
+ "nvidia-ml-py>=12",
93
+ "timm>=1.0",
94
+ "einops>=0.7",
76
95
  ]
77
96
 
78
97
  # PyPI ships CPU-only torch on Windows; pin the CUDA index there (pip: see requirements.txt).
@@ -108,4 +127,6 @@ typeCheckingMode = "strict"
108
127
 
109
128
  [tool.pytest.ini_options]
110
129
  testpaths = ["tests"]
111
- pythonpath = ["src"]
130
+ # "." so `tests` imports as a package: several suites share fixtures via `from tests.x import y`,
131
+ # and without it those modules fail to collect and silently stop running.
132
+ pythonpath = ["src", "."]
@@ -0,0 +1,14 @@
1
+ """Inline Core: the generation engine behind Inline.
2
+
3
+ Takes a typed node graph and returns immutable takes. See PLAN.md for the architecture and
4
+ docs/contract.md for the Storyline API.
5
+ """
6
+
7
+ from importlib.metadata import PackageNotFoundError, version
8
+
9
+ try:
10
+ #: Resolved from the installed package, so pyproject.toml stays the only place a release is
11
+ #: bumped. An editable install records this at install time; reinstall after bumping.
12
+ __version__ = version("inline-core")
13
+ except PackageNotFoundError: # a source tree that was never installed
14
+ __version__ = "0.0.0"
@@ -47,6 +47,10 @@ _SMART_RESIDENT_MIN_VRAM_GB = 6.0
47
47
  # ~half the fp16 weight bytes. Deliberately generous so the estimate errs toward a lighter plan.
48
48
  _ACTIVATION_HEADROOM_GB = 2.5
49
49
  _INT8_FACTOR = 0.5
50
+ # NF4 (bitsandbytes) stores 4-bit weights plus per-block scales, so ~0.55 bytes per parameter
51
+ # against fp16's 2. The rung exists for the very large checkpoints (FLUX.2 dev and friends) that
52
+ # int8 still cannot fit; it is CUDA-only and, like int8, never combined with CPU offload.
53
+ _NF4_FACTOR = 0.28
50
54
 
51
55
 
52
56
  def _system_ram_gb() -> float | None:
@@ -202,9 +206,10 @@ class MemoryPolicy(DevicePolicy):
202
206
  return None
203
207
  cap = max(0.0, budget - _ACTIVATION_HEADROOM_GB)
204
208
  big = (fp.diffusion_bytes + fp.text_encoder_bytes) / 1e9
205
- vae = fp.vae_bytes / 1e9
206
- full = big + vae
207
- int8 = big * _INT8_FACTOR + vae
209
+ # The VAE and a ControlNet are never quantized, so they cost the same under every plan.
210
+ fixed = (fp.vae_bytes + fp.controlnet_bytes) / 1e9
211
+ full = big + fixed
212
+ int8 = big * _INT8_FACTOR + fixed
208
213
  forced = _env_profile() is not None # explicit --profile pins the profile; fit picks quant
209
214
 
210
215
  def prof(auto: Profile) -> Profile:
@@ -221,7 +226,14 @@ class MemoryPolicy(DevicePolicy):
221
226
  int8, budget, True,
222
227
  "Weights are int8-quantized to fit this GPU's VRAM.",
223
228
  )
224
- # int8 still won't fit resident -> CPU-offload streaming. Only viable if the (unquantized)
229
+ nf4 = big * _NF4_FACTOR + fixed
230
+ if nf4 <= cap:
231
+ return FitEstimate(
232
+ "nf4", Quantization.NF4, OffloadMode.NONE, prof(Profile.LOWVRAM),
233
+ nf4, budget, True,
234
+ "Weights are 4-bit (NF4) quantized to fit this GPU's VRAM.",
235
+ )
236
+ # Even 4-bit won't fit resident -> CPU-offload streaming. Only viable if the (unquantized)
225
237
  # model fits in system RAM, since sequential offload holds the off-GPU weights there.
226
238
  ram = self._ram_gb
227
239
  if ram is not None and full > ram:
@@ -87,10 +87,14 @@ class ModelFootprint:
87
87
  diffusion_bytes: int = 0
88
88
  text_encoder_bytes: int = 0
89
89
  vae_bytes: int = 0
90
+ #: A ControlNet loaded alongside the denoiser. Never quantized, so it counts full in every plan.
91
+ controlnet_bytes: int = 0
90
92
 
91
93
  @property
92
94
  def total_bytes(self) -> int:
93
- return self.diffusion_bytes + self.text_encoder_bytes + self.vae_bytes
95
+ return (
96
+ self.diffusion_bytes + self.text_encoder_bytes + self.vae_bytes + self.controlnet_bytes
97
+ )
94
98
 
95
99
 
96
100
  @dataclass(frozen=True)
@@ -12,7 +12,7 @@ from typing import Any
12
12
 
13
13
  from ..takes import Take
14
14
  from .registry import Registry
15
- from .schema import Graph, Node
15
+ from .schema import Graph, Node, PortKind
16
16
 
17
17
 
18
18
  class NodeCache(ABC):
@@ -40,8 +40,15 @@ def _canonical_params(node: Node, registry: Registry) -> dict[str, Any]:
40
40
 
41
41
 
42
42
  def is_cache_eligible(node: Node, registry: Registry) -> bool:
43
- """False when any seed param resolves to a negative (random) value."""
43
+ """False when a control map is wired, or any seed param resolves to a negative (random) value.
44
+
45
+ A node driven by a control map re-runs every time: the user iterates on the pose/depth and
46
+ expects each run to apply the current control, so a cached take would read as "control not
47
+ taking effect" (even a re-render at the same seed must re-apply it)."""
44
48
  descriptor = registry.get(node.type)
49
+ for port in descriptor.inputs:
50
+ if port.kind is PortKind.CONTROL and node.inputs.get(port.id):
51
+ return False
45
52
  defaults = descriptor.defaults()
46
53
  for key in descriptor.seed_keys():
47
54
  value = node.params.get(key, defaults.get(key))
@@ -81,3 +88,29 @@ def node_cache_key(
81
88
  digest = hashlib.sha256(json.dumps(payload, sort_keys=True, default=str).encode()).hexdigest()
82
89
  memo[node_id] = digest
83
90
  return digest
91
+
92
+
93
+ def asset_content_hashes(graph: Graph) -> dict[str, str]:
94
+ """The byte hash of each file-backed source node's asset, keyed by node id. Feeds
95
+ ``node_cache_key`` so the cache invalidates when a file's *content* changes even though its path
96
+ did not (a re-rendered control map, an in-place-replaced input image). Only ``ref="path"`` refs
97
+ are hashable; a missing file is skipped - its path still keys the node through its params."""
98
+ import os
99
+
100
+ hashes: dict[str, str] = {}
101
+ for node in graph.nodes:
102
+ asset = node.params.get("asset")
103
+ if not isinstance(asset, dict) or asset.get("ref") != "path":
104
+ continue
105
+ path = asset.get("path")
106
+ if isinstance(path, str) and os.path.isfile(path):
107
+ hashes[node.id] = _file_hash(path)
108
+ return hashes
109
+
110
+
111
+ def _file_hash(path: str) -> str:
112
+ digest = hashlib.sha256()
113
+ with open(path, "rb") as handle:
114
+ for chunk in iter(lambda: handle.read(1 << 20), b""):
115
+ digest.update(chunk)
116
+ return digest.hexdigest()
@@ -6,6 +6,7 @@ It orchestrates cheap work inline. A model node's runner submits the denoise to
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
+ import logging
9
10
  from dataclasses import replace
10
11
  from typing import Any
11
12
 
@@ -13,12 +14,14 @@ from ..errors import CancelledError, GraphValidationError, InlineCoreError
13
14
  from ..runtime.context import ExecutionContext
14
15
  from ..runtime.progress import CancelledEvent, ErrorEvent, NodeDoneEvent, RunDoneEvent
15
16
  from ..runtime.run import NodeRuntimeState, RunState, RunStatus, StateTrackingEmitter
16
- from .cache import NodeCache, is_cache_eligible, node_cache_key
17
+ from .cache import NodeCache, asset_content_hashes, is_cache_eligible, node_cache_key
17
18
  from .registry import Registry
18
19
  from .schema import Graph, Node
19
20
  from .topo import topo_sort, upstream_closure
20
21
  from .validate import validate
21
22
 
23
+ logger = logging.getLogger(__name__)
24
+
22
25
 
23
26
  class Executor:
24
27
  def __init__(self, registry: Registry, cache: NodeCache) -> None:
@@ -33,10 +36,13 @@ class Executor:
33
36
  order = self._plan(graph, target, state)
34
37
  state.status = RunStatus.RUNNING
35
38
  outputs: dict[str, dict[str, Any]] = {}
39
+ # Hash the input files once so the node cache is content-addressed: a re-rendered
40
+ # control map (or any replaced input) invalidates even when its path is unchanged.
41
+ asset_hashes = asset_content_hashes(graph)
36
42
  for node_id in order:
37
43
  if ctx.cancel.cancelled:
38
44
  raise CancelledError("Run cancelled.")
39
- self._run_node(graph, node_id, outputs, run_ctx)
45
+ self._run_node(graph, node_id, outputs, run_ctx, asset_hashes)
40
46
  emitter.emit(RunDoneEvent(run_id=ctx.run_id))
41
47
  except CancelledError:
42
48
  emitter.emit(CancelledEvent(run_id=ctx.run_id))
@@ -44,6 +50,12 @@ class Executor:
44
50
  emitter.emit(ErrorEvent(run_id=ctx.run_id, message=str(error), node_id=error.node_id))
45
51
  except InlineCoreError as error:
46
52
  emitter.emit(ErrorEvent(run_id=ctx.run_id, message=str(error)))
53
+ except Exception as error: # noqa: BLE001
54
+ # A runner that raises a non-InlineCoreError (e.g. a diffusers/HF load error) must still
55
+ # terminate the run: otherwise it escapes to the worker thread, the terminal event is
56
+ # never sent, and the run wedges in "queued" while the UI hangs on "loading model".
57
+ logger.exception("Run %s failed with an unhandled error", ctx.run_id)
58
+ emitter.emit(ErrorEvent(run_id=ctx.run_id, message=str(error) or type(error).__name__))
47
59
 
48
60
  def _plan(self, graph: Graph, target: str, state: RunState) -> list[str]:
49
61
  validate(graph, target, self._registry)
@@ -59,6 +71,7 @@ class Executor:
59
71
  node_id: str,
60
72
  outputs: dict[str, dict[str, Any]],
61
73
  ctx: ExecutionContext,
74
+ asset_hashes: dict[str, str],
62
75
  ) -> None:
63
76
  node = graph.node(node_id)
64
77
  runner = self._registry.runner(node.type)
@@ -66,8 +79,7 @@ class Executor:
66
79
 
67
80
  key: str | None = None
68
81
  if runner.produces_takes and is_cache_eligible(node, self._registry):
69
- # TODO(phase1): pass real asset content hashes so identity is content-addressed.
70
- key = node_cache_key(graph, node_id, self._registry, asset_hashes={})
82
+ key = node_cache_key(graph, node_id, self._registry, asset_hashes=asset_hashes)
71
83
  cached = self._cache.get(key)
72
84
  if cached is not None:
73
85
  ctx.emitter.emit(
@@ -26,6 +26,7 @@ class PortKind(str, Enum):
26
26
  LORA = "lora"
27
27
  CONDITIONING = "conditioning"
28
28
  LATENT = "latent"
29
+ CONTROL = "control"
29
30
 
30
31
 
31
32
  def port_satisfies(source: PortKind, target: PortKind) -> bool:
@@ -33,7 +34,10 @@ def port_satisfies(source: PortKind, target: PortKind) -> bool:
33
34
  if source == target:
34
35
  return True
35
36
  # a single image satisfies a list input (a one-element list)
36
- return source is PortKind.IMAGE and target is PortKind.IMAGE_LIST
37
+ if source is PortKind.IMAGE and target is PortKind.IMAGE_LIST:
38
+ return True
39
+ # a control input accepts any image output (the control map is just an image)
40
+ return source is PortKind.IMAGE and target is PortKind.CONTROL
37
41
 
38
42
 
39
43
  @dataclass(frozen=True)
@@ -15,6 +15,8 @@ import hashlib
15
15
  import json
16
16
  from pathlib import Path
17
17
 
18
+ from ..config import models_dir
19
+
18
20
  # Category subfolders scanned under the models root. These are the keys a param's `options_from`
19
21
  # may reference (see graph/primitives.py); ensure_dirs() creates them so drop-in is obvious.
20
22
  CATEGORIES: tuple[str, ...] = (
@@ -96,3 +98,22 @@ class ModelCatalog:
96
98
  # A sharded model (config + shards) is one entry, named for its folder.
97
99
  names.append(entry.name)
98
100
  return sorted(names)
101
+
102
+
103
+ def resolve_picked(category: str, chosen: object) -> Path | None:
104
+ """A model dropdown's stored value, resolved to a file under ``category``.
105
+
106
+ The select serves bare filenames, but projects written before it did store the full relative
107
+ path, and joining one of those under the category doubles the prefix into a path that cannot
108
+ exist. So: try the value as given under the category, then its bare name there, then the value
109
+ as a path in its own right. Returns None when nothing matches, which callers must not stringify
110
+ (``str(None)`` reaching a loader reads as a corrupt file rather than a stale pick).
111
+ """
112
+ name = str(chosen or "").strip()
113
+ if not name:
114
+ return None
115
+ root = models_dir() / category
116
+ for candidate in (root / name, root / Path(name).name, Path(name)):
117
+ if candidate.exists():
118
+ return candidate
119
+ return None
@@ -59,6 +59,11 @@ class CheckpointReader:
59
59
  def keys(self) -> list[str]:
60
60
  return list(self._index)
61
61
 
62
+ def shapes(self) -> dict[str, list[int]]:
63
+ """Every tensor's shape, from the header alone - no torch, no tensor read. Lets a caller
64
+ infer a checkpoint's architecture (layer counts, widths) before deciding how to load it."""
65
+ return {key: list(entry.get("shape") or []) for key, entry in self._index.items()}
66
+
62
67
  def get_tensor(self, key: str, device: str | None = None) -> Any:
63
68
  """One tensor, read straight from its byte range into a fresh buffer."""
64
69
  import torch
@@ -0,0 +1,26 @@
1
+ """Requirements for the client-side "Control Space" node: just the suggested ControlNet model, so
2
+ the node can offer a one-click download when ``models/controlnet/`` is empty. Torch-free (pure
3
+ filesystem), so it registers even on a runtime-less install - a download only needs the models dir.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from ..config import models_dir
12
+ from .requirements import ModelComponent
13
+ from .zimage.requirements import controlnet_component
14
+
15
+
16
+ class ControlSpaceProvider:
17
+ """The ControlNet a Control Space render needs downstream - offered as a suggested download."""
18
+
19
+ def components(self, params: dict[str, object] | None = None) -> list[ModelComponent]:
20
+ return [controlnet_component()]
21
+
22
+ def download_target(self, component: ModelComponent) -> Path:
23
+ return models_dir() / component.category
24
+
25
+ def estimate(self, policy: Any) -> dict[str, Any] | None:
26
+ return None
@@ -0,0 +1 @@
1
+ """FLUX.2: one node over the whole checkpoint family (klein 4B/9B, their base builds, KV, dev)."""