inline-core 1.2.67__tar.gz → 1.2.68__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 (266) hide show
  1. {inline_core-1.2.67 → inline_core-1.2.68}/PKG-INFO +1 -1
  2. {inline_core-1.2.67 → inline_core-1.2.68}/pyproject.toml +1 -1
  3. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/config.py +23 -2
  4. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/catalog.py +102 -15
  5. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/minimaxh3/pipeline.py +24 -8
  6. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/minimaxh3/runner.py +6 -1
  7. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/pipeline_runtime.py +51 -4
  8. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/runtime/progress.py +11 -1
  9. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/runtime/run.py +3 -0
  10. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/server/app.py +74 -7
  11. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/server/manager.py +85 -8
  12. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/server/serialize.py +14 -0
  13. inline_core-1.2.68/src/inline_core/studio/activity.py +396 -0
  14. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/studio/fal.py +75 -17
  15. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/studio/generation.py +99 -45
  16. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/studio/handlers.py +42 -3
  17. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/studio/schema.py +18 -1
  18. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/studio/store.py +58 -3
  19. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/studio/training.py +149 -9
  20. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/studio/training_store.py +2 -0
  21. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/training/cache.py +46 -0
  22. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/training/dataset.py +8 -0
  23. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/training/h3.py +1 -1
  24. inline_core-1.2.68/src/inline_core/training/precache_store.py +136 -0
  25. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/training/protocol.py +5 -0
  26. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/training/trainer.py +29 -0
  27. inline_core-1.2.68/tests/test_catalog.py +146 -0
  28. inline_core-1.2.68/tests/test_encoder_parking.py +77 -0
  29. inline_core-1.2.68/tests/test_manager_queue.py +152 -0
  30. inline_core-1.2.68/tests/test_precache_store.py +113 -0
  31. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_staged_residency.py +92 -0
  32. inline_core-1.2.68/tests/test_studio_activity.py +270 -0
  33. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_studio_schema.py +25 -0
  34. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_studio_training.py +79 -0
  35. inline_core-1.2.68/tests/test_training_snapshots.py +68 -0
  36. inline_core-1.2.67/tests/test_catalog.py +0 -64
  37. {inline_core-1.2.67 → inline_core-1.2.68}/.gitignore +0 -0
  38. {inline_core-1.2.67 → inline_core-1.2.68}/.python-version +0 -0
  39. {inline_core-1.2.67 → inline_core-1.2.68}/CLAUDE.md +0 -0
  40. {inline_core-1.2.67 → inline_core-1.2.68}/README.md +0 -0
  41. {inline_core-1.2.67 → inline_core-1.2.68}/main.py +0 -0
  42. {inline_core-1.2.67 → inline_core-1.2.68}/scripts/flux2_train_matrix.py +0 -0
  43. {inline_core-1.2.67 → inline_core-1.2.68}/scripts/minimax_h3_lora_check.py +0 -0
  44. {inline_core-1.2.67 → inline_core-1.2.68}/scripts/minimax_h3_train_matrix.py +0 -0
  45. {inline_core-1.2.67 → inline_core-1.2.68}/scripts/reference.py +0 -0
  46. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/__init__.py +0 -0
  47. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/components/__init__.py +0 -0
  48. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/components/conditioning.py +0 -0
  49. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/components/interfaces.py +0 -0
  50. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/device/__init__.py +0 -0
  51. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/device/auto.py +0 -0
  52. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/device/detect.py +0 -0
  53. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/device/memory.py +0 -0
  54. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/device/policy.py +0 -0
  55. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/device/probe.py +0 -0
  56. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/device/types.py +0 -0
  57. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/errors.py +0 -0
  58. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/extensions/__init__.py +0 -0
  59. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/extensions/api.py +0 -0
  60. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/extensions/constraints.py +0 -0
  61. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/extensions/fetch.py +0 -0
  62. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/extensions/handlers.py +0 -0
  63. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/extensions/importer.py +0 -0
  64. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/extensions/install.py +0 -0
  65. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/extensions/loader.py +0 -0
  66. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/extensions/manifest.py +0 -0
  67. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/extensions/models.py +0 -0
  68. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/extensions/paths.py +0 -0
  69. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/extensions/resolve.py +0 -0
  70. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/extensions/scanner.py +0 -0
  71. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/extensions/state.py +0 -0
  72. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/extensions/tools.py +0 -0
  73. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/ffmpeg.py +0 -0
  74. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/graph/__init__.py +0 -0
  75. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/graph/cache.py +0 -0
  76. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/graph/descriptor.py +0 -0
  77. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/graph/executor.py +0 -0
  78. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/graph/loader_runners.py +0 -0
  79. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/graph/primitives.py +0 -0
  80. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/graph/registry.py +0 -0
  81. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/graph/runners.py +0 -0
  82. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/graph/schema.py +0 -0
  83. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/graph/topo.py +0 -0
  84. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/graph/validate.py +0 -0
  85. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/media.py +0 -0
  86. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/__init__.py +0 -0
  87. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/checkpoint.py +0 -0
  88. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/controlspace.py +0 -0
  89. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/flux2/__init__.py +0 -0
  90. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/flux2/controlnet.py +0 -0
  91. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/flux2/embeds.py +0 -0
  92. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/flux2/provider.py +0 -0
  93. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/flux2/requirements.py +0 -0
  94. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/flux2/runner.py +0 -0
  95. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/flux2/variants.py +0 -0
  96. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/keymap.py +0 -0
  97. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/krea2/__init__.py +0 -0
  98. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/krea2/convert.py +0 -0
  99. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/krea2/depth_control.py +0 -0
  100. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/krea2/img2img.py +0 -0
  101. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/krea2/provider.py +0 -0
  102. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/krea2/requirements.py +0 -0
  103. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/krea2/runner.py +0 -0
  104. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/loaders.py +0 -0
  105. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/lora.py +0 -0
  106. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/minimaxh3/__init__.py +0 -0
  107. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/minimaxh3/adaln.py +0 -0
  108. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/minimaxh3/keys.py +0 -0
  109. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/minimaxh3/load.py +0 -0
  110. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/minimaxh3/lora_keys.py +0 -0
  111. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/minimaxh3/provider.py +0 -0
  112. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/minimaxh3/requirements.py +0 -0
  113. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/minimaxh3/vae_keys.py +0 -0
  114. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/minimaxh3/vendor/__init__.py +0 -0
  115. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/minimaxh3/vendor/autoencoder_kl_minimax_h3.py +0 -0
  116. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/minimaxh3/vendor/autoencoder_kl_minimax_h3_audio.py +0 -0
  117. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/minimaxh3/vendor/before_denoise.py +0 -0
  118. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/minimaxh3/vendor/before_encoder.py +0 -0
  119. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/minimaxh3/vendor/decoders.py +0 -0
  120. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/minimaxh3/vendor/denoise.py +0 -0
  121. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/minimaxh3/vendor/encoders.py +0 -0
  122. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/minimaxh3/vendor/modular_blocks_minimax_h3.py +0 -0
  123. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/minimaxh3/vendor/modular_pipeline.py +0 -0
  124. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/minimaxh3/vendor/packing.py +0 -0
  125. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/minimaxh3/vendor/packing_ref2va.py +0 -0
  126. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/minimaxh3/vendor/scheduling_minimax_h3.py +0 -0
  127. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/minimaxh3/vendor/transformer_minimax_h3.py +0 -0
  128. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/offload.py +0 -0
  129. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/prepared.py +0 -0
  130. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/preprocess/__init__.py +0 -0
  131. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/preprocess/requirements.py +0 -0
  132. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/preprocess/runner.py +0 -0
  133. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/references.py +0 -0
  134. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/requirements.py +0 -0
  135. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/sampling.py +0 -0
  136. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/video_params.py +0 -0
  137. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/zimage/__init__.py +0 -0
  138. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/zimage/primitives.py +0 -0
  139. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/zimage/provider.py +0 -0
  140. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/zimage/requirements.py +0 -0
  141. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/models/zimage/runner.py +0 -0
  142. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/parallel/__init__.py +0 -0
  143. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/parallel/config.py +0 -0
  144. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/parallel/group.py +0 -0
  145. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/parallel/launch.py +0 -0
  146. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/parallel/protocol.py +0 -0
  147. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/parallel/registry.py +0 -0
  148. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/parallel/worker.py +0 -0
  149. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/runtime/__init__.py +0 -0
  150. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/runtime/context.py +0 -0
  151. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/runtime/file_store.py +0 -0
  152. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/runtime/store.py +0 -0
  153. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/runtime/video_encode.py +0 -0
  154. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/sampling/__init__.py +0 -0
  155. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/sampling/batch.py +0 -0
  156. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/server/__init__.py +0 -0
  157. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/server/__main__.py +0 -0
  158. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/server/assets.py +0 -0
  159. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/server/bootstrap.py +0 -0
  160. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/server/frontend.py +0 -0
  161. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/server/rpc.py +0 -0
  162. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/server/run_store.py +0 -0
  163. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/studio/__init__.py +0 -0
  164. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/studio/assets.py +0 -0
  165. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/studio/config.py +0 -0
  166. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/studio/frames.py +0 -0
  167. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/studio/graph_build.py +0 -0
  168. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/studio/image_meta.py +0 -0
  169. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/studio/models.py +0 -0
  170. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/studio/moodboard.py +0 -0
  171. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/studio/peaks.py +0 -0
  172. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/studio/recipe.py +0 -0
  173. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/studio/system_stats.py +0 -0
  174. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/studio/timeline/__init__.py +0 -0
  175. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/studio/timeline/compose.py +0 -0
  176. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/studio/timeline/ffmpeg.py +0 -0
  177. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/studio/timeline/render.py +0 -0
  178. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/studio/timeline/resolve.py +0 -0
  179. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/takes.py +0 -0
  180. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/training/__init__.py +0 -0
  181. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/training/__main__.py +0 -0
  182. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/training/arch.py +0 -0
  183. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/training/caption.py +0 -0
  184. {inline_core-1.2.67 → inline_core-1.2.68}/src/inline_core/training/models.py +0 -0
  185. {inline_core-1.2.67 → inline_core-1.2.68}/tests/conftest.py +0 -0
  186. {inline_core-1.2.67 → inline_core-1.2.68}/tests/helpers.py +0 -0
  187. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_cache.py +0 -0
  188. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_checkpoint.py +0 -0
  189. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_config.py +0 -0
  190. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_device_detect.py +0 -0
  191. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_executor.py +0 -0
  192. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_extension_api.py +0 -0
  193. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_extension_install.py +0 -0
  194. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_extension_manifest.py +0 -0
  195. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_extension_resolve.py +0 -0
  196. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_extension_scanner.py +0 -0
  197. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_extension_spine.py +0 -0
  198. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_extension_state.py +0 -0
  199. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_file_store.py +0 -0
  200. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_flux2_controlnet.py +0 -0
  201. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_flux2_folder.py +0 -0
  202. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_flux2_resolve.py +0 -0
  203. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_flux2_runner.py +0 -0
  204. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_flux2_training.py +0 -0
  205. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_flux2_variants.py +0 -0
  206. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_frontend_serving.py +0 -0
  207. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_hidden_nodes.py +0 -0
  208. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_keymap.py +0 -0
  209. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_krea2_convert.py +0 -0
  210. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_krea2_depth_control.py +0 -0
  211. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_krea2_requirements.py +0 -0
  212. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_krea2_runner.py +0 -0
  213. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_loader_runners.py +0 -0
  214. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_loaders.py +0 -0
  215. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_lora.py +0 -0
  216. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_lora_download.py +0 -0
  217. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_memory_policy.py +0 -0
  218. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_minimaxh3_adaln.py +0 -0
  219. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_minimaxh3_keys.py +0 -0
  220. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_minimaxh3_load.py +0 -0
  221. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_minimaxh3_lora_keys.py +0 -0
  222. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_minimaxh3_nodes.py +0 -0
  223. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_minimaxh3_training.py +0 -0
  224. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_model_requirements.py +0 -0
  225. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_offload_prepared.py +0 -0
  226. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_output_kind_contract.py +0 -0
  227. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_parallel_group.py +0 -0
  228. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_pipeline_cache.py +0 -0
  229. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_primitives.py +0 -0
  230. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_recipe.py +0 -0
  231. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_references.py +0 -0
  232. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_rpc_bridge.py +0 -0
  233. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_run_store.py +0 -0
  234. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_sampling.py +0 -0
  235. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_schema.py +0 -0
  236. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_server.py +0 -0
  237. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_studio_assets.py +0 -0
  238. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_studio_fal.py +0 -0
  239. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_studio_frames.py +0 -0
  240. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_studio_generation.py +0 -0
  241. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_studio_graph_build.py +0 -0
  242. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_studio_models.py +0 -0
  243. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_studio_moodboard.py +0 -0
  244. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_studio_multi_reference.py +0 -0
  245. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_studio_node_size.py +0 -0
  246. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_studio_peaks.py +0 -0
  247. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_studio_rpc.py +0 -0
  248. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_studio_store.py +0 -0
  249. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_studio_timeline.py +0 -0
  250. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_take_bytes.py +0 -0
  251. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_topo.py +0 -0
  252. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_training_arch.py +0 -0
  253. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_training_dataset.py +0 -0
  254. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_training_models.py +0 -0
  255. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_training_resolve.py +0 -0
  256. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_validate.py +0 -0
  257. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_video_encode.py +0 -0
  258. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_video_params.py +0 -0
  259. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_webui_install.py +0 -0
  260. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_xfuser_sampler.py +0 -0
  261. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_zimage_primitives.py +0 -0
  262. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_zimage_resolve.py +0 -0
  263. {inline_core-1.2.67 → inline_core-1.2.68}/tests/test_zimage_runner.py +0 -0
  264. {inline_core-1.2.67 → inline_core-1.2.68}/uv.lock +0 -0
  265. {inline_core-1.2.67 → inline_core-1.2.68}/webui.bat +0 -0
  266. {inline_core-1.2.67 → inline_core-1.2.68}/webui.sh +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: inline-core
3
- Version: 1.2.67
3
+ Version: 1.2.68
4
4
  Summary: The generation engine behind Inline Studio.
5
5
  License-Expression: GPL-3.0-or-later
6
6
  Requires-Python: >=3.11
@@ -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.67"
4
+ version = "1.2.68"
5
5
  description = "The generation engine behind Inline Studio."
6
6
  readme = "README.md"
7
7
  license = "GPL-3.0-or-later"
@@ -7,12 +7,33 @@ from pathlib import Path
7
7
 
8
8
 
9
9
  def models_dir() -> Path:
10
- """The models root scanned on start (category subfolders inside). `INLINE_MODELS_DIR`, else
11
- `./models`. Users drop their own weight files here; nothing is downloaded."""
10
+ """The **writable** models root: where downloads and trained LoRAs land. `INLINE_MODELS_DIR`,
11
+ else `./models`. Users drop their own weight files here; nothing is downloaded."""
12
12
  env = os.environ.get("INLINE_MODELS_DIR")
13
13
  return Path(env).expanduser() if env else Path("models")
14
14
 
15
15
 
16
+ def models_dirs() -> list[Path]:
17
+ """Every root scanned for installed weights, writable one first.
18
+
19
+ Pointing `INLINE_MODELS_DIR` (or `--models-dir`) somewhere else used to hide `./models`
20
+ entirely, so a user with weights in both places could only ever see one set. The extra roots
21
+ are read-only: only `models_dir()` is ever written to.
22
+ """
23
+ roots = [models_dir()]
24
+ default = Path("models")
25
+ for extra in (default, *_extra_models_dirs()):
26
+ if extra not in roots and extra.is_dir():
27
+ roots.append(extra)
28
+ return roots
29
+
30
+
31
+ def _extra_models_dirs() -> list[Path]:
32
+ """Additional read-only roots from `INLINE_EXTRA_MODELS_DIRS` (os.pathsep-separated)."""
33
+ raw = os.environ.get("INLINE_EXTRA_MODELS_DIRS", "")
34
+ return [Path(p).expanduser() for p in raw.split(os.pathsep) if p.strip()]
35
+
36
+
16
37
  def data_dir() -> Path:
17
38
  """Engine-owned working data (the run DB, takes). `INLINE_DATA_DIR`, else `./.inline`."""
18
39
  env = os.environ.get("INLINE_DATA_DIR")
@@ -13,9 +13,11 @@ from __future__ import annotations
13
13
 
14
14
  import hashlib
15
15
  import json
16
+ from collections.abc import Sequence
16
17
  from pathlib import Path
18
+ from typing import Any
17
19
 
18
- from ..config import models_dir
20
+ from ..config import models_dirs
19
21
 
20
22
  # Category subfolders scanned under the models root. These are the keys a param's `options_from`
21
23
  # may reference (see graph/primitives.py); ensure_dirs() creates them so drop-in is obvious.
@@ -29,6 +31,9 @@ CATEGORIES: tuple[str, ...] = (
29
31
  "controlnet",
30
32
  "upscale_models",
31
33
  "embeddings",
34
+ # Written by the preprocess runner rather than the user, but it holds real weights and the
35
+ # panel is meant to show everything on disk.
36
+ "annotators",
32
37
  )
33
38
 
34
39
  # Extensions we treat as model weights. A folder counts as a model if it contains one of these.
@@ -45,34 +50,111 @@ def _folder_has_weight(path: Path) -> bool:
45
50
  return any(_is_weight(child) for child in path.rglob("*"))
46
51
 
47
52
 
53
+ def _file_node(path: Path) -> dict[str, Any]:
54
+ stat = path.stat()
55
+ return {
56
+ "name": path.name,
57
+ "path": str(path),
58
+ "kind": "file",
59
+ "sizeBytes": stat.st_size,
60
+ "mtime": int(stat.st_mtime * 1000),
61
+ }
62
+
63
+
64
+ def _dir_node(path: Path, name: str) -> dict[str, Any] | None:
65
+ """A directory as a tree node, or None when it holds nothing worth showing."""
66
+ if not path.is_dir():
67
+ return None
68
+ children: list[dict[str, Any]] = []
69
+ try:
70
+ entries = sorted(path.iterdir(), key=lambda p: p.name.lower())
71
+ except OSError:
72
+ return None
73
+ for entry in entries:
74
+ # Caches like loras/.cache and text_encoders/.snap are noise in a panel about weights.
75
+ if entry.name.startswith("."):
76
+ continue
77
+ if _is_weight(entry):
78
+ children.append(_file_node(entry))
79
+ elif entry.is_dir():
80
+ child = _dir_node(entry, entry.name)
81
+ if child is not None:
82
+ children.append(child)
83
+ if not children:
84
+ return None
85
+ return {
86
+ "name": name,
87
+ "path": str(path),
88
+ "kind": "dir",
89
+ "children": children,
90
+ "fileCount": sum(1 for c in children if c["kind"] == "file"),
91
+ }
92
+
93
+
48
94
  class ModelCatalog:
49
- """Scans the models root and answers "what's installed" per category.
95
+ """Scans the models roots and answers "what's installed" per category.
50
96
 
51
97
  Cheap to construct; nothing touches disk until ``ensure_dirs`` or ``rescan``/``scan``. Results
52
- are cached so ``list`` and ``fingerprint`` are hits between scans.
98
+ are cached so ``list`` and ``fingerprint`` are hits between scans. Several roots may be scanned
99
+ (see ``config.models_dirs``); only the first is ever written to.
53
100
  """
54
101
 
55
- def __init__(self, root: Path) -> None:
56
- self._root = Path(root)
102
+ def __init__(self, root: Path | Sequence[Path]) -> None:
103
+ roots = [Path(root)] if isinstance(root, str | Path) else [Path(r) for r in root]
104
+ self._roots = roots or [Path("models")]
57
105
  self._entries: dict[str, list[str]] = {category: [] for category in CATEGORIES}
58
106
 
59
107
  @property
60
108
  def root(self) -> Path:
61
- return self._root
109
+ """The writable root. Downloads and trained LoRAs land here."""
110
+ return self._roots[0]
111
+
112
+ @property
113
+ def roots(self) -> list[Path]:
114
+ return list(self._roots)
62
115
 
63
116
  def ensure_dirs(self) -> None:
64
- """Create the root and every category subfolder, so users have somewhere to drop weights."""
117
+ """Create the writable root and its category subfolders, so drop-in has somewhere to go."""
65
118
  for category in CATEGORIES:
66
- (self._root / category).mkdir(parents=True, exist_ok=True)
119
+ (self.root / category).mkdir(parents=True, exist_ok=True)
67
120
 
68
121
  def rescan(self) -> dict[str, list[str]]:
69
- """Re-read every category from disk, cache the result, and return it."""
122
+ """Re-read every category from every root, cache the result, and return it."""
70
123
  entries: dict[str, list[str]] = {}
71
124
  for category in CATEGORIES:
72
- entries[category] = self._scan_category(self._root / category)
125
+ names: list[str] = []
126
+ for root in self._roots:
127
+ for name in self._scan_category(root / category):
128
+ if name not in names:
129
+ names.append(name)
130
+ entries[category] = sorted(names)
73
131
  self._entries = entries
74
132
  return entries
75
133
 
134
+ def tree(self) -> list[dict[str, Any]]:
135
+ """Every root as a nested, read-only listing for the Models panel.
136
+
137
+ Separate from ``rescan`` because the select only needs flat names, while the panel wants
138
+ nesting, sizes and mtimes; keeping them apart stops the hot path paying for the detail.
139
+ """
140
+ out: list[dict[str, Any]] = []
141
+ for index, root in enumerate(self._roots):
142
+ categories = [
143
+ node
144
+ for category in CATEGORIES
145
+ if (node := _dir_node(root / category, category)) is not None
146
+ ]
147
+ out.append(
148
+ {
149
+ "path": str(root),
150
+ "label": root.name or str(root),
151
+ "writable": index == 0,
152
+ "exists": root.is_dir(),
153
+ "categories": categories,
154
+ }
155
+ )
156
+ return out
157
+
76
158
  # app.py calls scan() in the lifespan; rescan() is the same work exposed for tests/callers that
77
159
  # want the mapping back. Keep both so neither call site has to know about the other.
78
160
  def scan(self) -> dict[str, list[str]]:
@@ -92,6 +174,8 @@ class ModelCatalog:
92
174
  return []
93
175
  names: list[str] = []
94
176
  for entry in directory.iterdir():
177
+ if entry.name.startswith("."):
178
+ continue
95
179
  if _is_weight(entry):
96
180
  names.append(entry.name)
97
181
  elif entry.is_dir() and _folder_has_weight(entry):
@@ -112,8 +196,11 @@ def resolve_picked(category: str, chosen: object) -> Path | None:
112
196
  name = str(chosen or "").strip()
113
197
  if not name:
114
198
  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
199
+ # Every root, writable one first, so a pick resolves wherever the user actually keeps it.
200
+ for models_root in models_dirs():
201
+ root = models_root / category
202
+ for candidate in (root / name, root / Path(name).name):
203
+ if candidate.exists():
204
+ return candidate
205
+ bare = Path(name)
206
+ return bare if bare.exists() else None
@@ -452,7 +452,7 @@ def _denoiser_name(blocks: Any) -> str:
452
452
  raise ComponentError("This MiniMax H3 blockset declares no denoiser component.")
453
453
 
454
454
 
455
- def render_staged(pipe: Any, device: Any, **call: Any) -> Any:
455
+ def render_staged(pipe: Any, device: Any, cancel_check: Any = None, **call: Any) -> Any:
456
456
  """Encode the prompt, get the conditioner off the card, then denoise with the card to itself.
457
457
 
458
458
  H3's conditioner is a 32B model that rivals its denoiser, and holding both means the denoiser
@@ -463,21 +463,40 @@ def render_staged(pipe: Any, device: Any, **call: Any) -> Any:
463
463
  Falls back to the single call when the pipeline was not built staged, so a caller never has to
464
464
  ask which kind it holds.
465
465
  """
466
+ def check() -> None:
467
+ if cancel_check is not None:
468
+ cancel_check()
469
+
466
470
  phases = getattr(pipe, "_inline_phases", None)
467
471
  if phases is None:
472
+ check()
468
473
  return pipe(**call)
469
474
  head, tail = phases
470
475
  device = str(device)
471
476
 
472
- # Every kwarg goes to BOTH halves, not just the first. `set_timesteps` lives in the tail, so a
473
- # head-only handoff left it reading the descriptor default: a render that asked for 8 steps
474
- # silently took 49. The halves ignore what their own blocks do not declare, so this is safe.
475
- state = head(**call)
476
477
  # Parked, not released: the next render needs it again, and 19.5 GB across the bus is seconds
477
478
  # against the minutes of streaming it buys back.
478
479
  denoiser = getattr(pipe, getattr(pipe, "_inline_denoiser", "transformer"))
479
480
  resident = getattr(pipe, "_inline_resident_denoiser", True)
480
481
 
482
+ # Each half claims the card *before* it runs rather than the previous one restoring the layout
483
+ # on its way out. Same two transfers per render in the steady state, but a cancelled denoise no
484
+ # longer pays ~40 GB of restore traffic before the exception surfaces, which read as the cancel
485
+ # being ignored for half a minute. A `.to()` onto the device a module already sits on is free.
486
+ check()
487
+ if resident:
488
+ denoiser.to("cpu")
489
+ pipe.text_encoder.to(device)
490
+ rt.free_vram()
491
+
492
+ # Every kwarg goes to BOTH halves, not just the first. `set_timesteps` lives in the tail, so a
493
+ # head-only handoff left it reading the descriptor default: a render that asked for 8 steps
494
+ # silently took 49. The halves ignore what their own blocks do not declare, so this is safe.
495
+ state = head(**call)
496
+ # The conditioner is a 32B model, so the encode alone runs for a while with no step hook to
497
+ # cancel from; without this a cancel there waits for the whole denoise as well.
498
+ check()
499
+
481
500
  # Parking the conditioner is worth doing either way: it frees the card for activations even when
482
501
  # the denoiser is too big to take it. Moving the denoiser across is only right when it fits.
483
502
  pipe.text_encoder.to("cpu")
@@ -487,9 +506,6 @@ def render_staged(pipe: Any, device: Any, **call: Any) -> Any:
487
506
  try:
488
507
  return tail(state, **call)
489
508
  finally:
490
- if resident:
491
- denoiser.to("cpu")
492
- pipe.text_encoder.to(device)
493
509
  rt.free_vram()
494
510
 
495
511
 
@@ -341,7 +341,12 @@ class MiniMaxH3Runner(NodeRunner):
341
341
  rt.attach_step_progress(pipe, on_step)
342
342
  started = time.perf_counter()
343
343
  try:
344
- state = render_staged(pipe, self._policy.placement('denoiser').device, **call)
344
+ state = render_staged(
345
+ pipe,
346
+ self._policy.placement('denoiser').device,
347
+ cancel_check=lambda: rt.raise_if_cancelled(ctx),
348
+ **call,
349
+ )
345
350
  except CancelledError:
346
351
  rt.free_vram()
347
352
  raise
@@ -66,6 +66,38 @@ def is_resident(policy: DevicePolicy) -> bool:
66
66
  )
67
67
 
68
68
 
69
+ # VRAM the denoise should still have spare, with the encoder kept resident, before parking is worth
70
+ # its two multi-GB copies. Sized for activations at a large resolution, not for the weights.
71
+ _ENCODER_RESIDENT_HEADROOM_MB = 8 * 1024
72
+
73
+
74
+ def module_bytes(module: Any) -> int:
75
+ """On-device size of a module's parameters and buffers."""
76
+ try:
77
+ params = sum(p.numel() * p.element_size() for p in module.parameters())
78
+ buffers = sum(b.numel() * b.element_size() for b in module.buffers())
79
+ return params + buffers
80
+ except Exception: # noqa: BLE001 - a size estimate must never break a run
81
+ return 0
82
+
83
+
84
+ def should_park_encoder(policy: DevicePolicy, text_encoder: Any) -> bool:
85
+ """Whether to evict the text encoder to the CPU for the denoise.
86
+
87
+ Parking buys VRAM at the cost of a GPU->CPU copy now and a CPU->GPU copy on the next run, which
88
+ on a large card is seconds of PCIe traffic for headroom that was never contended. So it is only
89
+ worth it when keeping the encoder would actually crowd the denoise.
90
+ """
91
+ if policy.profile is Profile.LOWVRAM:
92
+ return True
93
+ free_mb = policy.free_vram_mb()
94
+ if free_mb is None:
95
+ return True # unmeasurable card: keep the conservative behaviour
96
+ # Measured with the encoder already on the card, so this is exactly what the denoise would get
97
+ # if it stayed. Parking would hand back its own size on top.
98
+ return free_mb < _ENCODER_RESIDENT_HEADROOM_MB
99
+
100
+
69
101
  def configure_pipeline(pipe: Any, policy: DevicePolicy) -> None:
70
102
  """Place the pipeline and enable the low-VRAM savers the policy asks for."""
71
103
  placement = policy.placement("denoiser")
@@ -340,9 +372,12 @@ def encoded_prompt_kwargs(
340
372
  text_encoder.to(device)
341
373
  with torch.no_grad():
342
374
  kwargs = encode(device)
343
- # torchao's .to() round-trip on quantized weights is unreliable, so only the encoder moves.
344
- text_encoder.to("cpu")
345
- free_vram()
375
+ park = should_park_encoder(policy, text_encoder)
376
+ if park:
377
+ # torchao's .to() round-trip on quantized weights is unreliable, so only the encoder
378
+ # moves.
379
+ text_encoder.to("cpu")
380
+ free_vram()
346
381
  except Exception as error: # noqa: BLE001 - an optimization must never break generation
347
382
  logger.warning(
348
383
  "Text-encoder GPU encode failed (%s); denoising with the encoder resident.", error
@@ -350,7 +385,17 @@ def encoded_prompt_kwargs(
350
385
  try_call(text_encoder.to, device)
351
386
  return fallback()
352
387
 
353
- logger.info("Text encoder parked on the CPU for the denoise | host RAM %.1fGB", host_ram_gb())
388
+ if park:
389
+ logger.info(
390
+ "Text encoder parked on the CPU for the denoise | host RAM %.1fGB", host_ram_gb()
391
+ )
392
+ else:
393
+ logger.info(
394
+ "Text encoder kept on %s for the denoise (%.1fGB VRAM free) | host RAM %.1fGB",
395
+ device,
396
+ (policy.free_vram_mb() or 0) / 1024,
397
+ host_ram_gb(),
398
+ )
354
399
  return kwargs
355
400
 
356
401
 
@@ -634,6 +679,7 @@ __all__ = [
634
679
  "is_resident",
635
680
  "load_image",
636
681
  "lora_stack",
682
+ "module_bytes",
637
683
  "oom_message",
638
684
  "path_or_none",
639
685
  "peak_vram_gb",
@@ -642,6 +688,7 @@ __all__ = [
642
688
  "reset_peak_vram",
643
689
  "resolve_seed",
644
690
  "shrink_vae_tiles",
691
+ "should_park_encoder",
645
692
  "smaller_resolutions",
646
693
  "supports_prompt_embeds",
647
694
  "text_encoder_detached",
@@ -43,6 +43,14 @@ class NodeDoneEvent:
43
43
  takes: list[Take] = field(default_factory=list)
44
44
 
45
45
 
46
+ @dataclass(frozen=True)
47
+ class RunStartedEvent:
48
+ """A queued run reached the worker. Without it nothing distinguishes queued from running, so a
49
+ client cannot tell a long model load from a run still waiting behind another."""
50
+
51
+ run_id: str
52
+
53
+
46
54
  @dataclass(frozen=True)
47
55
  class RunDoneEvent:
48
56
  run_id: str
@@ -60,7 +68,9 @@ class ErrorEvent:
60
68
  node_id: str | None = None
61
69
 
62
70
 
63
- RunEvent = ProgressEvent | NodeDoneEvent | RunDoneEvent | CancelledEvent | ErrorEvent
71
+ RunEvent = (
72
+ ProgressEvent | NodeDoneEvent | RunStartedEvent | RunDoneEvent | CancelledEvent | ErrorEvent
73
+ )
64
74
 
65
75
 
66
76
  class ProgressEmitter(ABC):
@@ -14,6 +14,7 @@ from .progress import (
14
14
  ProgressEvent,
15
15
  RunDoneEvent,
16
16
  RunEvent,
17
+ RunStartedEvent,
17
18
  )
18
19
 
19
20
 
@@ -89,6 +90,8 @@ def apply_event(state: RunState, event: RunEvent) -> None:
89
90
  known = {t.id for t in state.takes}
90
91
  state.takes.extend(t for t in event.takes if t.id not in known)
91
92
  _recompute_fraction(state)
93
+ elif isinstance(event, RunStartedEvent):
94
+ state.status = RunStatus.RUNNING
92
95
  elif isinstance(event, RunDoneEvent):
93
96
  state.status = RunStatus.DONE
94
97
  state.fraction = 1.0
@@ -19,7 +19,7 @@ from fastapi import FastAPI, Request, Response, WebSocket, WebSocketDisconnect
19
19
  from fastapi.responses import FileResponse, JSONResponse
20
20
  from fastapi.staticfiles import StaticFiles
21
21
 
22
- from ..config import models_dir
22
+ from ..config import models_dirs
23
23
  from ..device.memory import MemoryPolicy
24
24
  from ..device.policy import DevicePolicy
25
25
  from ..errors import GraphValidationError, UnknownNodeType
@@ -34,7 +34,7 @@ from .assets import AssetStore
34
34
  from .manager import RunConflict, RunManager
35
35
  from .rpc import EventBroadcaster, RpcRouter
36
36
  from .run_store import RunStore
37
- from .serialize import descriptor_json, event_json, run_json, take_json
37
+ from .serialize import descriptor_json, event_json, run_json, run_summary_json, take_json
38
38
 
39
39
  # GET /v1/runs/<id> (the client's run-status poll) - but not /events or nested paths.
40
40
  _RUN_POLL_PATH = re.compile(r"^/v1/runs/[^/]+$")
@@ -135,17 +135,23 @@ def create_app(
135
135
  # reports no model requirements, which is what a torch-less install already showed.
136
136
  reqs = requirements if requirements is not None else RequirementsRegistry()
137
137
  assets = AssetStore(Path(asset_dir or "./.inline-assets"))
138
- catalog = ModelCatalog(Path(models_root) if models_root else models_dir())
138
+ # An explicit models_root is that caller's whole world; otherwise scan every configured root so
139
+ # a custom --models-dir does not hide ./models.
140
+ catalog = ModelCatalog([Path(models_root)] if models_root else models_dirs())
139
141
  takes_root = Path(takes_dir or "./.inline-takes")
140
142
  manager = RunManager(registry, cache, policy, store=run_store, takes=FileTakeStore(takes_root))
141
143
  rpc = rpc or RpcRouter()
142
144
  events = events or EventBroadcaster()
143
145
  # Host/GPU telemetry for the Trainer tab; only meaningful with the SPA (studio) backend wired.
144
146
  stats = SystemStats(events) if studio_store is not None else None
147
+ # Assigned further down with the studio wiring; the lifespan closure reads it at startup.
148
+ activity_registry: Any = None
145
149
 
146
150
  @asynccontextmanager
147
151
  async def lifespan(app: FastAPI): # noqa: ANN202
148
152
  manager.bind_loop(asyncio.get_running_loop())
153
+ if activity_registry is not None:
154
+ activity_registry.bind_loop(asyncio.get_running_loop())
149
155
  catalog.ensure_dirs()
150
156
  catalog.scan()
151
157
  if stats is not None:
@@ -199,9 +205,15 @@ def create_app(
199
205
  target = body.get("target")
200
206
  if not isinstance(target, str):
201
207
  return _error("invalid_request", "'target' is required.", 422)
208
+ meta = body.get("meta")
202
209
  try:
203
210
  graph = parse_graph(body.get("graph"))
204
- record, created = manager.submit(graph, target, body.get("clientRunId"))
211
+ record, created = manager.submit(
212
+ graph,
213
+ target,
214
+ body.get("clientRunId"),
215
+ meta if isinstance(meta, dict) else None,
216
+ )
205
217
  except GraphValidationError as error:
206
218
  return _error("invalid_graph", str(error), 422, node_id=error.node_id)
207
219
  except RunConflict as error:
@@ -211,6 +223,15 @@ def create_app(
211
223
  status_code=201 if created else 200,
212
224
  )
213
225
 
226
+ @app.get("/v1/runs")
227
+ async def list_runs() -> Response:
228
+ runs = [
229
+ run_summary_json(r.state, manager.queue_position(r.state.run_id))
230
+ for r in manager.list_runs()
231
+ if not r.done
232
+ ]
233
+ return JSONResponse({"runs": runs})
234
+
214
235
  @app.get("/v1/runs/{run_id}")
215
236
  async def get_run(run_id: str) -> Response:
216
237
  record = manager.get(run_id)
@@ -300,6 +321,7 @@ def create_app(
300
321
  if studio_store is not None:
301
322
  from ..extensions.handlers import register_extension_handlers
302
323
  from ..extensions.install import Installer
324
+ from ..studio.activity import ActivityRegistry
303
325
  from ..studio.fal import FalGeneration
304
326
  from ..studio.generation import CoreGeneration
305
327
  from ..studio.handlers import register_studio_handlers
@@ -318,21 +340,49 @@ def create_app(
318
340
  def core_status() -> dict[str, Any]:
319
341
  return {"running": True, "url": ""}
320
342
 
343
+ def rescan_models() -> dict[str, Any]:
344
+ """Re-read the models roots and tell every client the registry moved.
345
+
346
+ The catalog caches its scan, and only a download or a finished training run refreshed
347
+ it - so a weight file dropped in by hand stayed invisible until Core restarted.
348
+ """
349
+ catalog.rescan()
350
+ version = _version(registry, catalog)
351
+ events.broadcast("events:modelsChanged", {"registryVersion": version})
352
+ return {"registryVersion": version}
353
+
354
+ # Observes the manager, so a run submitted straight to POST /v1/runs is listed and
355
+ # cancellable here too, not just the ones this UI started.
356
+ activity = ActivityRegistry(studio_store, events)
357
+ activity.observe(manager)
358
+ activity.set_canceller("core", manager.cancel)
359
+ core_generation = CoreGeneration(studio_store, manager, events, registry, activity)
360
+ fal_generation = FalGeneration(studio_store, events, activity)
361
+ training_service = Training(
362
+ studio_store, events, on_output=catalog.rescan, activity=activity
363
+ )
364
+ activity.set_canceller("fal", fal_generation.cancel_run)
365
+ activity.set_canceller("training", training_service.cancel)
366
+ activity_registry = activity
367
+
321
368
  register_studio_handlers(
322
369
  rpc,
323
370
  studio_store,
324
371
  core_models=core_models,
325
372
  core_status=core_status,
326
- generation=CoreGeneration(studio_store, manager, events, registry),
327
- fal_generation=FalGeneration(studio_store, events),
373
+ generation=core_generation,
374
+ fal_generation=fal_generation,
328
375
  timeline=Timeline(studio_store, events),
329
- training=Training(studio_store, events, on_output=catalog.rescan),
376
+ training=training_service,
377
+ activity=activity,
330
378
  # Explicit model downloads write into models/; rescan so new files bump the registry.
331
379
  # The policy lets the requirements popup show a memory fit estimate before a load;
332
380
  # the requirements registry says which node types have models at all.
333
381
  model_downloads=ModelDownloads(
334
382
  events, on_change=catalog.rescan, policy=policy, requirements=reqs
335
383
  ),
384
+ model_tree=catalog.tree,
385
+ model_rescan=rescan_models,
336
386
  )
337
387
 
338
388
  register_extension_handlers(
@@ -347,6 +397,23 @@ def create_app(
347
397
  ),
348
398
  )
349
399
 
400
+ @app.get("/download/snapshot/{run_id}/{step}")
401
+ async def download_snapshot(run_id: str, step: int) -> Response:
402
+ # A mid-run LoRA lives in the project's working dir, which /media does not surface and
403
+ # no model picker scans, so a download is the only way to get one out of the browser.
404
+ try:
405
+ root = (studio_store.folder() / "training_runs" / run_id / "snapshots").resolve()
406
+ except RuntimeError:
407
+ return Response("No project open", status_code=404)
408
+ target = (root / f"step-{step:06d}.safetensors").resolve()
409
+ if target.parent != root: # no traversal out of the run's own folder
410
+ return Response("Forbidden", status_code=403)
411
+ if not target.is_file():
412
+ return Response("Not found", status_code=404)
413
+ return FileResponse(
414
+ target, filename=target.name, media_type="application/octet-stream"
415
+ )
416
+
350
417
  @app.get("/media/{media_path:path}")
351
418
  async def media(media_path: str, request: Request) -> Response:
352
419
  try: