inline-core 1.2.63__tar.gz → 1.2.65__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 (259) hide show
  1. {inline_core-1.2.63 → inline_core-1.2.65}/PKG-INFO +2 -1
  2. {inline_core-1.2.63 → inline_core-1.2.65}/pyproject.toml +11 -4
  3. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/device/detect.py +82 -1
  4. inline_core-1.2.65/src/inline_core/device/probe.py +76 -0
  5. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/minimaxh3/load.py +8 -0
  6. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/server/__main__.py +20 -7
  7. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/studio/handlers.py +2 -0
  8. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/studio/store.py +40 -1
  9. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/studio/training.py +36 -0
  10. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/training/arch.py +16 -0
  11. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/training/cache.py +12 -2
  12. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/training/caption.py +19 -2
  13. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/training/dataset.py +15 -5
  14. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/training/h3.py +112 -27
  15. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/training/models.py +60 -0
  16. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/training/trainer.py +22 -4
  17. inline_core-1.2.65/tests/test_device_detect.py +323 -0
  18. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_minimaxh3_training.py +116 -0
  19. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_studio_store.py +32 -0
  20. inline_core-1.2.65/tests/test_studio_training.py +175 -0
  21. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_training_models.py +65 -0
  22. inline_core-1.2.65/tests/test_webui_install.py +338 -0
  23. {inline_core-1.2.63 → inline_core-1.2.65}/uv.lock +47 -85
  24. {inline_core-1.2.63 → inline_core-1.2.65}/webui.bat +192 -7
  25. {inline_core-1.2.63 → inline_core-1.2.65}/webui.sh +229 -7
  26. inline_core-1.2.63/tests/test_device_detect.py +0 -126
  27. inline_core-1.2.63/tests/test_studio_training.py +0 -81
  28. inline_core-1.2.63/tests/test_webui_install.py +0 -154
  29. {inline_core-1.2.63 → inline_core-1.2.65}/.gitignore +0 -0
  30. {inline_core-1.2.63 → inline_core-1.2.65}/.python-version +0 -0
  31. {inline_core-1.2.63 → inline_core-1.2.65}/CLAUDE.md +0 -0
  32. {inline_core-1.2.63 → inline_core-1.2.65}/README.md +0 -0
  33. {inline_core-1.2.63 → inline_core-1.2.65}/main.py +0 -0
  34. {inline_core-1.2.63 → inline_core-1.2.65}/scripts/flux2_train_matrix.py +0 -0
  35. {inline_core-1.2.63 → inline_core-1.2.65}/scripts/minimax_h3_lora_check.py +0 -0
  36. {inline_core-1.2.63 → inline_core-1.2.65}/scripts/minimax_h3_train_matrix.py +0 -0
  37. {inline_core-1.2.63 → inline_core-1.2.65}/scripts/reference.py +0 -0
  38. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/__init__.py +0 -0
  39. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/components/__init__.py +0 -0
  40. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/components/conditioning.py +0 -0
  41. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/components/interfaces.py +0 -0
  42. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/config.py +0 -0
  43. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/device/__init__.py +0 -0
  44. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/device/auto.py +0 -0
  45. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/device/memory.py +0 -0
  46. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/device/policy.py +0 -0
  47. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/device/types.py +0 -0
  48. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/errors.py +0 -0
  49. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/extensions/__init__.py +0 -0
  50. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/extensions/api.py +0 -0
  51. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/extensions/constraints.py +0 -0
  52. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/extensions/fetch.py +0 -0
  53. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/extensions/handlers.py +0 -0
  54. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/extensions/importer.py +0 -0
  55. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/extensions/install.py +0 -0
  56. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/extensions/loader.py +0 -0
  57. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/extensions/manifest.py +0 -0
  58. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/extensions/models.py +0 -0
  59. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/extensions/paths.py +0 -0
  60. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/extensions/resolve.py +0 -0
  61. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/extensions/scanner.py +0 -0
  62. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/extensions/state.py +0 -0
  63. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/extensions/tools.py +0 -0
  64. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/ffmpeg.py +0 -0
  65. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/graph/__init__.py +0 -0
  66. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/graph/cache.py +0 -0
  67. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/graph/descriptor.py +0 -0
  68. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/graph/executor.py +0 -0
  69. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/graph/loader_runners.py +0 -0
  70. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/graph/primitives.py +0 -0
  71. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/graph/registry.py +0 -0
  72. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/graph/runners.py +0 -0
  73. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/graph/schema.py +0 -0
  74. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/graph/topo.py +0 -0
  75. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/graph/validate.py +0 -0
  76. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/media.py +0 -0
  77. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/__init__.py +0 -0
  78. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/catalog.py +0 -0
  79. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/checkpoint.py +0 -0
  80. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/controlspace.py +0 -0
  81. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/flux2/__init__.py +0 -0
  82. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/flux2/controlnet.py +0 -0
  83. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/flux2/embeds.py +0 -0
  84. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/flux2/provider.py +0 -0
  85. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/flux2/requirements.py +0 -0
  86. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/flux2/runner.py +0 -0
  87. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/flux2/variants.py +0 -0
  88. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/keymap.py +0 -0
  89. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/krea2/__init__.py +0 -0
  90. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/krea2/convert.py +0 -0
  91. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/krea2/depth_control.py +0 -0
  92. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/krea2/img2img.py +0 -0
  93. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/krea2/provider.py +0 -0
  94. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/krea2/requirements.py +0 -0
  95. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/krea2/runner.py +0 -0
  96. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/loaders.py +0 -0
  97. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/lora.py +0 -0
  98. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/minimaxh3/__init__.py +0 -0
  99. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/minimaxh3/adaln.py +0 -0
  100. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/minimaxh3/keys.py +0 -0
  101. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/minimaxh3/pipeline.py +0 -0
  102. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/minimaxh3/provider.py +0 -0
  103. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/minimaxh3/requirements.py +0 -0
  104. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/minimaxh3/runner.py +0 -0
  105. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/minimaxh3/vae_keys.py +0 -0
  106. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/minimaxh3/vendor/__init__.py +0 -0
  107. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/minimaxh3/vendor/autoencoder_kl_minimax_h3.py +0 -0
  108. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/minimaxh3/vendor/autoencoder_kl_minimax_h3_audio.py +0 -0
  109. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/minimaxh3/vendor/before_denoise.py +0 -0
  110. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/minimaxh3/vendor/before_encoder.py +0 -0
  111. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/minimaxh3/vendor/decoders.py +0 -0
  112. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/minimaxh3/vendor/denoise.py +0 -0
  113. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/minimaxh3/vendor/encoders.py +0 -0
  114. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/minimaxh3/vendor/modular_blocks_minimax_h3.py +0 -0
  115. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/minimaxh3/vendor/modular_pipeline.py +0 -0
  116. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/minimaxh3/vendor/packing.py +0 -0
  117. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/minimaxh3/vendor/packing_ref2va.py +0 -0
  118. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/minimaxh3/vendor/scheduling_minimax_h3.py +0 -0
  119. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/minimaxh3/vendor/transformer_minimax_h3.py +0 -0
  120. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/offload.py +0 -0
  121. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/pipeline_runtime.py +0 -0
  122. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/prepared.py +0 -0
  123. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/preprocess/__init__.py +0 -0
  124. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/preprocess/requirements.py +0 -0
  125. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/preprocess/runner.py +0 -0
  126. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/references.py +0 -0
  127. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/requirements.py +0 -0
  128. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/sampling.py +0 -0
  129. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/video_params.py +0 -0
  130. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/zimage/__init__.py +0 -0
  131. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/zimage/primitives.py +0 -0
  132. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/zimage/provider.py +0 -0
  133. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/zimage/requirements.py +0 -0
  134. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/models/zimage/runner.py +0 -0
  135. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/parallel/__init__.py +0 -0
  136. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/parallel/config.py +0 -0
  137. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/parallel/group.py +0 -0
  138. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/parallel/launch.py +0 -0
  139. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/parallel/protocol.py +0 -0
  140. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/parallel/registry.py +0 -0
  141. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/parallel/worker.py +0 -0
  142. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/runtime/__init__.py +0 -0
  143. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/runtime/context.py +0 -0
  144. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/runtime/file_store.py +0 -0
  145. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/runtime/progress.py +0 -0
  146. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/runtime/run.py +0 -0
  147. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/runtime/store.py +0 -0
  148. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/runtime/video_encode.py +0 -0
  149. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/sampling/__init__.py +0 -0
  150. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/sampling/batch.py +0 -0
  151. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/server/__init__.py +0 -0
  152. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/server/app.py +0 -0
  153. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/server/assets.py +0 -0
  154. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/server/bootstrap.py +0 -0
  155. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/server/frontend.py +0 -0
  156. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/server/manager.py +0 -0
  157. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/server/rpc.py +0 -0
  158. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/server/run_store.py +0 -0
  159. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/server/serialize.py +0 -0
  160. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/studio/__init__.py +0 -0
  161. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/studio/assets.py +0 -0
  162. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/studio/config.py +0 -0
  163. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/studio/fal.py +0 -0
  164. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/studio/frames.py +0 -0
  165. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/studio/generation.py +0 -0
  166. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/studio/graph_build.py +0 -0
  167. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/studio/image_meta.py +0 -0
  168. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/studio/models.py +0 -0
  169. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/studio/moodboard.py +0 -0
  170. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/studio/peaks.py +0 -0
  171. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/studio/recipe.py +0 -0
  172. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/studio/schema.py +0 -0
  173. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/studio/system_stats.py +0 -0
  174. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/studio/timeline/__init__.py +0 -0
  175. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/studio/timeline/compose.py +0 -0
  176. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/studio/timeline/ffmpeg.py +0 -0
  177. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/studio/timeline/render.py +0 -0
  178. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/studio/timeline/resolve.py +0 -0
  179. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/studio/training_store.py +0 -0
  180. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/takes.py +0 -0
  181. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/training/__init__.py +0 -0
  182. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/training/__main__.py +0 -0
  183. {inline_core-1.2.63 → inline_core-1.2.65}/src/inline_core/training/protocol.py +0 -0
  184. {inline_core-1.2.63 → inline_core-1.2.65}/tests/conftest.py +0 -0
  185. {inline_core-1.2.63 → inline_core-1.2.65}/tests/helpers.py +0 -0
  186. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_cache.py +0 -0
  187. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_catalog.py +0 -0
  188. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_checkpoint.py +0 -0
  189. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_config.py +0 -0
  190. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_executor.py +0 -0
  191. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_extension_api.py +0 -0
  192. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_extension_install.py +0 -0
  193. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_extension_manifest.py +0 -0
  194. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_extension_resolve.py +0 -0
  195. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_extension_scanner.py +0 -0
  196. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_extension_spine.py +0 -0
  197. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_extension_state.py +0 -0
  198. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_file_store.py +0 -0
  199. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_flux2_controlnet.py +0 -0
  200. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_flux2_folder.py +0 -0
  201. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_flux2_resolve.py +0 -0
  202. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_flux2_runner.py +0 -0
  203. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_flux2_training.py +0 -0
  204. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_flux2_variants.py +0 -0
  205. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_frontend_serving.py +0 -0
  206. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_hidden_nodes.py +0 -0
  207. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_keymap.py +0 -0
  208. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_krea2_convert.py +0 -0
  209. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_krea2_depth_control.py +0 -0
  210. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_krea2_requirements.py +0 -0
  211. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_krea2_runner.py +0 -0
  212. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_loader_runners.py +0 -0
  213. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_loaders.py +0 -0
  214. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_lora.py +0 -0
  215. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_lora_download.py +0 -0
  216. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_memory_policy.py +0 -0
  217. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_minimaxh3_adaln.py +0 -0
  218. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_minimaxh3_keys.py +0 -0
  219. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_minimaxh3_load.py +0 -0
  220. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_minimaxh3_nodes.py +0 -0
  221. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_model_requirements.py +0 -0
  222. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_offload_prepared.py +0 -0
  223. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_output_kind_contract.py +0 -0
  224. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_parallel_group.py +0 -0
  225. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_pipeline_cache.py +0 -0
  226. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_primitives.py +0 -0
  227. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_recipe.py +0 -0
  228. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_references.py +0 -0
  229. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_rpc_bridge.py +0 -0
  230. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_run_store.py +0 -0
  231. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_sampling.py +0 -0
  232. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_schema.py +0 -0
  233. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_server.py +0 -0
  234. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_staged_residency.py +0 -0
  235. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_studio_assets.py +0 -0
  236. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_studio_fal.py +0 -0
  237. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_studio_frames.py +0 -0
  238. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_studio_generation.py +0 -0
  239. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_studio_graph_build.py +0 -0
  240. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_studio_models.py +0 -0
  241. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_studio_moodboard.py +0 -0
  242. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_studio_multi_reference.py +0 -0
  243. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_studio_node_size.py +0 -0
  244. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_studio_peaks.py +0 -0
  245. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_studio_rpc.py +0 -0
  246. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_studio_schema.py +0 -0
  247. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_studio_timeline.py +0 -0
  248. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_take_bytes.py +0 -0
  249. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_topo.py +0 -0
  250. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_training_arch.py +0 -0
  251. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_training_dataset.py +0 -0
  252. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_training_resolve.py +0 -0
  253. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_validate.py +0 -0
  254. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_video_encode.py +0 -0
  255. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_video_params.py +0 -0
  256. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_xfuser_sampler.py +0 -0
  257. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_zimage_primitives.py +0 -0
  258. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_zimage_resolve.py +0 -0
  259. {inline_core-1.2.63 → inline_core-1.2.65}/tests/test_zimage_runner.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: inline-core
3
- Version: 1.2.63
3
+ Version: 1.2.65
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,6 +8,7 @@ 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: av>=12; extra == 'all'
11
12
  Requires-Dist: bitsandbytes>=0.43; (platform_system != 'Darwin') and extra == 'all'
12
13
  Requires-Dist: controlnet-aux>=0.0.7; extra == 'all'
13
14
  Requires-Dist: diffusers==0.39.0; extra == 'all'
@@ -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.63"
4
+ version = "1.2.65"
5
5
  description = "The generation engine behind Inline Studio."
6
6
  readme = "README.md"
7
7
  license = "GPL-3.0-or-later"
@@ -82,6 +82,8 @@ all = [
82
82
  "accelerate>=0.30",
83
83
  "safetensors>=0.4",
84
84
  "torchao>=0.14",
85
+ # Clip decode for MiniMax H3 LoRA training, and H3's reference node.
86
+ "av>=12",
85
87
  "scipy>=1.11",
86
88
  "huggingface_hub>=0.23",
87
89
  "controlnet-aux>=0.0.7",
@@ -101,13 +103,18 @@ all = [
101
103
  ]
102
104
 
103
105
  # PyPI ships CPU-only torch on Windows; pin the CUDA index there (pip: see requirements.txt).
106
+ # cu126 rather than a newer index because it is the last one still built for sm_50..sm_70 (Maxwell
107
+ # through Volta), so it is the safe default for a card we know nothing about. Blackwell (sm_120) has
108
+ # no wheel here at all - webui.sh/.bat detect the compute capability and pass the matching index.
104
109
  [[tool.uv.index]]
105
- name = "pytorch-cu124"
106
- url = "https://download.pytorch.org/whl/cu124"
110
+ name = "pytorch-cu126"
111
+ url = "https://download.pytorch.org/whl/cu126"
107
112
  explicit = true
108
113
 
114
+ # Launcher installs pass `--no-sources-package torch` so their detected index wins over this pin;
115
+ # anything else added to this table would still apply to them.
109
116
  [tool.uv.sources]
110
- torch = [{ index = "pytorch-cu124", marker = "sys_platform == 'win32'" }]
117
+ torch = [{ index = "pytorch-cu126", marker = "sys_platform == 'win32'" }]
111
118
 
112
119
  [project.scripts]
113
120
  inline-studio = "inline_core.server.__main__:main"
@@ -2,6 +2,9 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from collections.abc import Iterable
6
+ from string import ascii_letters
7
+
5
8
  from .types import Device, DeviceKind
6
9
 
7
10
 
@@ -76,10 +79,88 @@ def cpu_only_torch_warning() -> str | None:
76
79
  "This install has a CPU-ONLY build of PyTorch, but an NVIDIA GPU was detected. "
77
80
  "Generation will run on the CPU and be roughly 100x slower. Reinstall torch from the CUDA "
78
81
  "index, e.g. `pip install --force-reinstall --extra-index-url "
79
- "https://download.pytorch.org/whl/cu124 torch` (or re-run `webui.sh --install`)."
82
+ "https://download.pytorch.org/whl/cu126 torch` (or re-run `webui.sh --install`)."
80
83
  )
81
84
 
82
85
 
86
+ def _parse_arch(arch: str) -> tuple[int, int] | None:
87
+ """``sm_120`` to ``(12, 0)``.
88
+
89
+ The minor is the **last digit** and the major is everything before it, so ``sm_100`` is (10, 0).
90
+ Reading left to right instead gives (1, 20) and puts the bug precisely on the Blackwell parts.
91
+ Tuned variants (``sm_90a``) drop their trailing letters.
92
+ """
93
+ digits = arch[3:].rstrip(ascii_letters)
94
+ if len(digits) < 2 or not digits.isdigit():
95
+ return None
96
+ return int(digits[:-1]), int(digits[-1])
97
+
98
+
99
+ def arch_list_covers(arches: Iterable[str], major: int, minor: int) -> bool:
100
+ """Whether a wheel's arch list has kernels that will run on a ``(major, minor)`` device.
101
+
102
+ CUDA binary compatibility runs **upward within one major only**: an ``sm_8x`` cubin executes on
103
+ any ``sm_8y`` where ``y >= x``. So ``sm_86`` covers an sm_89 Ada card, which is why exact
104
+ matching told every RTX 40-series owner their install was broken. Crossing a major never works,
105
+ so ``sm_90`` does not cover sm_120 and the Blackwell warning stays correct.
106
+
107
+ PTX entries (``compute_90``) JIT forward but are not matched here: they do not start with
108
+ ``sm_``, so a PTX-carrying wheel still warns. Rare, and warning is the safe way to be wrong.
109
+ """
110
+ for arch in arches:
111
+ parsed = _parse_arch(arch)
112
+ if parsed is not None and parsed[0] == major and parsed[1] <= minor:
113
+ return True
114
+ return False
115
+
116
+
117
+ def unsupported_arch_warning() -> str | None:
118
+ """A warning when the installed torch has no kernels for the GPU it is about to run on.
119
+
120
+ An RTX 50-series card (sm_120) under a wheel built for sm_50..sm_90 is the sharpest case: torch
121
+ reports a CUDA build and a visible device, so every check in ``cpu_only_torch_warning`` passes,
122
+ and the only clue the user gets is PyTorch's own late UserWarning followed by kernels that
123
+ cannot launch. Returns None on any uncertainty, same as the rest of this module.
124
+ """
125
+ try:
126
+ import torch
127
+
128
+ if getattr(torch.version, "hip", None):
129
+ return None # the sm_ vocabulary is NVIDIA's; HIP reports gfx arches through it
130
+ get_arch_list = getattr(torch.cuda, "get_arch_list", None)
131
+ get_capability = getattr(torch.cuda, "get_device_capability", None)
132
+ device_count = getattr(torch.cuda, "device_count", None)
133
+ if get_arch_list is None or get_capability is None or device_count is None:
134
+ return None
135
+ if device_count() < 1:
136
+ return None
137
+ arches = [str(arch) for arch in get_arch_list() if str(arch).startswith("sm_")]
138
+ if not arches:
139
+ return None # a CPU-only build; cpu_only_torch_warning owns that case
140
+ major, minor = get_capability(0)
141
+ target = f"sm_{major}{minor}"
142
+ if arch_list_covers(arches, major, minor):
143
+ return None
144
+ name = _device_name(torch) or "The detected NVIDIA GPU"
145
+ return (
146
+ f"{name} is compute capability {target}, but this install's PyTorch only has kernels "
147
+ f"for {' '.join(arches)}. Generation will fail or fall back to the CPU. Re-run "
148
+ "`webui.sh --install` (Windows: `.\\webui.bat --install`) to pick the wheel index that "
149
+ "matches the card, or force one with `--torch-index cu130`. On a Blackwell card whose "
150
+ "driver predates R580, use `cu128`, which still has sm_120 but is frozen at torch 2.11."
151
+ )
152
+ except Exception: # noqa: BLE001 - a diagnostic must never break startup
153
+ return None
154
+
155
+
156
+ def _device_name(torch: object) -> str | None:
157
+ try:
158
+ get_name = getattr(getattr(torch, "cuda", None), "get_device_name", None)
159
+ return str(get_name(0)) if get_name is not None else None
160
+ except Exception: # noqa: BLE001
161
+ return None
162
+
163
+
83
164
  def _nvidia_gpu_present() -> bool:
84
165
  """Best-effort check for NVIDIA hardware that does NOT rely on torch's CUDA support - that is
85
166
  the whole point, since we are called precisely when torch cannot see the GPU.
@@ -0,0 +1,76 @@
1
+ """Install-time probe: which torch is installed, and does it have kernels for this card?
2
+
3
+ Run as ``python -m inline_core.device.probe`` by both launchers. They cannot share the
4
+ compatibility rule any other way, and restating it in bash and batch is how the two drift.
5
+
6
+ Always prints one JSON object and always exits 0, so a shell branches on ``status`` rather than
7
+ parsing stderr. A **nonzero exit means the probe itself failed**, which callers must treat as
8
+ unknown and never as covered.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import re
15
+ import sys
16
+ from typing import Any
17
+
18
+ from .detect import arch_list_covers
19
+
20
+ #: A local version tag we recognise as a pytorch.org build, e.g. ``2.13.0+cu130`` or ``2.9.0+cpu``.
21
+ #: Only these are safe to replace automatically: a ROCm build, a nightly or a hand-built wheel will
22
+ #: also fail the arch check, and silently reinstalling over someone's deliberate choice is worse
23
+ #: than the wrong wheel.
24
+ _REPLACEABLE_LOCAL = re.compile(r"\+(?:cpu|cu\d+)$")
25
+
26
+
27
+ def probe() -> dict[str, Any]:
28
+ """What the launcher needs to decide whether to replace torch. Never raises."""
29
+ out: dict[str, Any] = {
30
+ "status": "unknown",
31
+ "torch": None,
32
+ "cuda": None,
33
+ "archList": [],
34
+ "capability": None,
35
+ "replaceable": False,
36
+ }
37
+ try:
38
+ import torch
39
+ except Exception: # noqa: BLE001 - no torch yet is a normal first install
40
+ out["status"] = "no-torch"
41
+ return out
42
+
43
+ try:
44
+ out["torch"] = str(torch.__version__)
45
+ out["cuda"] = getattr(torch.version, "cuda", None)
46
+ out["replaceable"] = bool(_REPLACEABLE_LOCAL.search(out["torch"]))
47
+ # HIP reports gfx arches through the sm_ call, so the rule does not apply. Never
48
+ # auto-replace a ROCm build.
49
+ if getattr(torch.version, "hip", None):
50
+ out["status"] = "rocm"
51
+ out["replaceable"] = False
52
+ return out
53
+ arches = [str(a) for a in torch.cuda.get_arch_list() if str(a).startswith("sm_")]
54
+ out["archList"] = arches
55
+ if not arches:
56
+ out["status"] = "cpu-only"
57
+ return out
58
+ if torch.cuda.device_count() < 1:
59
+ out["status"] = "no-gpu"
60
+ return out
61
+ major, minor = torch.cuda.get_device_capability(0)
62
+ out["capability"] = [major, minor]
63
+ out["status"] = "covered" if arch_list_covers(arches, major, minor) else "uncovered"
64
+ except Exception: # noqa: BLE001 - a broken torch is exactly what we are here to detect
65
+ out["status"] = "unknown"
66
+ return out
67
+
68
+
69
+ def main() -> int:
70
+ json.dump(probe(), sys.stdout)
71
+ sys.stdout.write("\n")
72
+ return 0
73
+
74
+
75
+ if __name__ == "__main__":
76
+ raise SystemExit(main())
@@ -157,6 +157,14 @@ def load_transformer(
157
157
 
158
158
  if lora_plan:
159
159
  _finish_fuse(model, lora_plan, fused)
160
+ # Announced because a fused LoRA is otherwise invisible: it changes the weights and nothing
161
+ # else, so a run with no adapter and a run with one that never arrived look identical in
162
+ # the log, and the only way to tell them apart was to render twice and compare.
163
+ logger.info(
164
+ "MiniMax H3: fused %d LoRA layer(s) from %s",
165
+ len(lora_plan),
166
+ ", ".join(f"{Path(ref.file).name}@{ref.strength:g}" for ref in loras),
167
+ )
160
168
  _assert_nothing_left_on_meta(model, filled)
161
169
  model.eval()
162
170
  return model
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import logging as _logging
5
6
  import os as _os
6
7
 
7
8
  # Default PyTorch to expandable CUDA segments before torch initializes its allocator (it reads this
@@ -19,7 +20,7 @@ _os.environ.setdefault("HF_HUB_DISABLE_XET", "1")
19
20
  import uvicorn
20
21
 
21
22
  from ..config import data_dir, server_host, server_port
22
- from ..device.detect import cpu_only_torch_warning
23
+ from ..device.detect import cpu_only_torch_warning, unsupported_arch_warning
23
24
  from ..device.memory import MemoryPolicy
24
25
  from ..extensions.loader import LoadedExtension
25
26
  from ..graph.cache import InMemoryCache
@@ -35,7 +36,20 @@ from .rpc import EventBroadcaster, RpcRouter
35
36
  from .run_store import SqliteRunStore
36
37
 
37
38
 
39
+ def _quiet_dependency_noise() -> None:
40
+ """Two third-party import-time warnings that are noise here, not signal.
41
+
42
+ diffusers cannot build its torchao ``torch.load`` allowlist because torchao dropped
43
+ ``uint4_layout``; we never load torchao-serialized checkpoints, and quantization itself is
44
+ unaffected. torch's pytree then warns about torchao registering Enum subclasses - torchao's to
45
+ fix, not ours. Both loggers have exactly one warning site, so this hides nothing else.
46
+ """
47
+ for name in ("diffusers.quantizers.torchao.torchao_quantizer", "torch.utils._pytree"):
48
+ _logging.getLogger(name).setLevel(_logging.ERROR)
49
+
50
+
38
51
  def main() -> None:
52
+ _quiet_dependency_noise() # before register_models, which is what pulls diffusers/torchao in
39
53
  policy = MemoryPolicy()
40
54
  registry = build_default_registry()
41
55
  data = data_dir()
@@ -47,20 +61,17 @@ def main() -> None:
47
61
  rpc = RpcRouter()
48
62
  events = EventBroadcaster()
49
63
  requirements = RequirementsRegistry()
50
- registered, extensions = register_models(
64
+ _, extensions = register_models(
51
65
  registry, take_store, policy, requirements=requirements, rpc=rpc, events=events
52
66
  )
53
- print(f"Registered models: {registered or 'none (source nodes only)'}")
54
67
  if extensions:
55
68
  print(f"Extensions: {_extension_summary(extensions)}")
56
69
  # A CPU-only torch wheel on a CUDA machine is a silent ~100x slowdown, so say it loudly here
57
70
  # rather than letting the user conclude the engine is just slow.
58
- torch_warning = cpu_only_torch_warning()
71
+ torch_warning = cpu_only_torch_warning() or unsupported_arch_warning()
59
72
  if torch_warning:
60
73
  print(f"WARNING: {torch_warning}")
61
74
  frontend_root = resolve_frontend_root()
62
- fe = frontend_root or "none (API only); use --front-end-root or install the frontend package"
63
- print(f"Frontend: {fe}")
64
75
  # The Studio app-backend: Core is the sole native backend (projects, frames, moodboard, assets,
65
76
  # generation, fal, timeline). Every InlineStudioApi channel is handled here.
66
77
  store = StudioStore(
@@ -68,7 +79,9 @@ def main() -> None:
68
79
  studio_config.workspace_dir(),
69
80
  default_core_url=studio_config.DEFAULT_CORE_URL,
70
81
  )
71
- print(f"Studio data: {studio_config.data_dir()} | workspace: {studio_config.workspace_dir()}")
82
+ # Reopen whatever was open before the restart, so a browser tab left open across it keeps
83
+ # working instead of failing every call with "No project is open."
84
+ store.restore_last_project()
72
85
  app = create_app(
73
86
  registry=registry,
74
87
  cache=InMemoryCache(),
@@ -88,6 +88,7 @@ def register_studio_handlers(
88
88
  reg("project:openZip", lambda: None)
89
89
  reg("project:listRecent", store.list_recent)
90
90
  reg("project:current", store.current_project)
91
+ reg("project:close", store.close_project)
91
92
  reg("project:mediaDirs", store.media_dirs)
92
93
  reg("project:export", lambda _path: None) # zip export: pending (see plan)
93
94
  reg("dialog:pickDirectory", lambda *_: str(cfg.workspace_dir()))
@@ -264,6 +265,7 @@ def register_studio_handlers(
264
265
  reg("training:createDataset", lambda inp: training.create_dataset(inp))
265
266
  reg("training:listItems", lambda did: training.list_items(did))
266
267
  reg("training:addItems", lambda did, aids: training.add_items(did, aids))
268
+ reg("training:addFromPath", lambda did, path: training.add_from_path(did, path))
267
269
  reg("training:removeItem", lambda iid: training.remove_item(iid))
268
270
  reg("training:setCaption", lambda iid, cap: training.set_caption(iid, cap))
269
271
  reg("training:autoCaption",
@@ -131,6 +131,7 @@ class StudioStore:
131
131
  project = {"id": pid, "name": name, "path": str(folder), "createdAt": now, "updatedAt": now}
132
132
  self._current = project
133
133
  self.record_recent(name, str(folder))
134
+ self._remember_last_project(str(folder))
134
135
  return project
135
136
 
136
137
  def open_project(self, selected: str) -> dict[str, Any]:
@@ -143,6 +144,7 @@ class StudioStore:
143
144
  project = self._load_project_row(folder)
144
145
  self._current = project
145
146
  self.record_recent(project["name"], str(folder))
147
+ self._remember_last_project(str(folder))
146
148
  return project
147
149
 
148
150
  def _load_project_row(self, folder: Path) -> dict[str, Any]:
@@ -160,7 +162,44 @@ class StudioStore:
160
162
  }
161
163
 
162
164
  def current_project(self) -> dict[str, Any] | None:
163
- return self._current
165
+ return self._current or self.restore_last_project()
166
+
167
+ def close_project(self) -> None:
168
+ self.close()
169
+ self._current = None
170
+ self._remember_last_project(None)
171
+
172
+ # --- last opened project ----------------------------------------------------------------------
173
+ # The open project is otherwise only in memory, so restarting Core left a still-open browser tab
174
+ # failing every call with "No project is open." Kept in its own file rather than settings.json,
175
+ # because _save_settings rewrites that from get_settings() and would drop any key it omits.
176
+
177
+ def _last_project_file(self) -> Path:
178
+ return self._app_data / "last_project"
179
+
180
+ def _remember_last_project(self, path: str | None) -> None:
181
+ file = self._last_project_file()
182
+ try:
183
+ if path:
184
+ file.write_text(path, encoding="utf-8")
185
+ elif file.exists():
186
+ file.unlink()
187
+ except OSError:
188
+ pass # never fail an open just because the marker could not be written
189
+
190
+ def restore_last_project(self) -> dict[str, Any] | None:
191
+ """Reopen the project left open at shutdown. Best-effort: a moved or deleted one is
192
+ forgotten and the launcher shows instead."""
193
+ if self._conn is not None:
194
+ return self._current
195
+ file = self._last_project_file()
196
+ if not file.exists():
197
+ return None
198
+ try:
199
+ return self.open_project(file.read_text(encoding="utf-8").strip())
200
+ except (OSError, ValueError, sqlite3.Error):
201
+ self._remember_last_project(None)
202
+ return None
164
203
 
165
204
  def media_dirs(self) -> dict[str, str]:
166
205
  if self._folder is None:
@@ -71,6 +71,42 @@ class Training:
71
71
  def add_items(self, dataset_id: str, asset_ids: list[str]) -> list[dict[str, Any]]:
72
72
  return ts.add_items(self._conn(), dataset_id, asset_ids)
73
73
 
74
+ def add_from_path(self, dataset_id: str, path: str) -> list[dict[str, Any]]:
75
+ """Import a folder of images and clips into the dataset, captions included.
76
+
77
+ The browser cannot hand over a folder, and uploading a clip dataset through it means
78
+ pushing gigabytes over HTTP to a server that can already see the disk. Paths come from the
79
+ client here the same way ``assets:importPaths`` already accepts them.
80
+ """
81
+ from . import assets as ax
82
+
83
+ folder = Path(path).expanduser()
84
+ if not folder.is_dir():
85
+ raise ValueError(f"Not a folder: {path}")
86
+ conn, project = self._conn(), self._store.folder()
87
+ media = [
88
+ p
89
+ for p in sorted(folder.iterdir())
90
+ if p.is_file() and ax.kind_for_file(str(p)) in ("image", "video")
91
+ ]
92
+ if not media:
93
+ raise ValueError(f"No images or clips in {path}")
94
+
95
+ imported = [(p, ax.import_file(conn, project, str(p), None)) for p in media]
96
+ added = ts.add_items(conn, dataset_id, [a["id"] for _p, a in imported if a])
97
+
98
+ # `NNNN.txt` beside `NNNN.png` is the caption, the convention the drag-drop path already
99
+ # follows. Only newly added items are touched, so re-importing cannot clobber an edit.
100
+ by_asset = {item["assetId"]: item for item in added}
101
+ for source, asset in imported:
102
+ item = by_asset.get(asset["id"]) if asset else None
103
+ sidecar = source.with_suffix(".txt")
104
+ if item and sidecar.is_file():
105
+ caption = sidecar.read_text(encoding="utf-8").strip()
106
+ if caption:
107
+ ts.set_caption(conn, item["id"], caption)
108
+ return ts.list_items(conn, dataset_id)
109
+
74
110
  def remove_item(self, item_id: str) -> None:
75
111
  ts.remove_item(self._conn(), item_id)
76
112
 
@@ -333,6 +333,22 @@ ARCHS: dict[str, TrainingArch] = {
333
333
  }
334
334
 
335
335
 
336
+ def clip_frames(arch: TrainingArch, seconds: Any) -> int:
337
+ """How many frames of a clip to train on, snapped to the arch's frame grid.
338
+
339
+ 1 for an arch with no clip support, which is what a still costs. For H3 the floor is a whole
340
+ 17-frame chunk plus the 5-frame head, so a shorter request rounds up to 0.92s rather than being
341
+ refused; the VAE has no way to encode less.
342
+ """
343
+ if arch.key != MINIMAX_H3:
344
+ return 1
345
+ from ..models.minimaxh3.vendor.packing import MINIMAX_H3_FPS
346
+ from ..models.minimaxh3.vendor.packing_ref2va import trim_reference_num_frames
347
+
348
+ wanted = round(float(seconds) * MINIMAX_H3_FPS) if seconds else 1
349
+ return trim_reference_num_frames(max(1, wanted))
350
+
351
+
336
352
  def get(key: str | None) -> TrainingArch:
337
353
  """The arch to train. Defaults to Z-Image so a run predating Krea 2 still resumes."""
338
354
  arch = ARCHS.get(key or Z_IMAGE)
@@ -7,6 +7,7 @@ them (see ``h3.py``). Either way the transformer loads into a card with nothing
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
+ from collections.abc import Callable
10
11
  from typing import Any
11
12
 
12
13
  from . import arch as archs
@@ -28,13 +29,22 @@ def build(
28
29
  *,
29
30
  flip: bool = False,
30
31
  dropout: float = 0.0,
32
+ clip_frames: int = 1,
33
+ clip_window: str = "start",
34
+ on_status: Callable[[str], None] | None = None,
31
35
  ) -> tuple[list[dict[str, Any]], dict[str, Any] | None, float]:
32
- """Return ``(items, unconditional, shift)``, all as CPU tensors, with the encoders freed."""
36
+ """Return ``(items, unconditional, shift)``, all as CPU tensors, with the encoders freed.
37
+
38
+ ``on_status`` reports phase progress to the caller, which forwards it over the JSON protocol.
39
+ Precaching a large dataset takes minutes, and a logger call would be dropped here: the trainer
40
+ subprocess configures no logging handler, so anything below WARNING goes nowhere.
41
+ """
33
42
  if arch == archs.MINIMAX_H3:
34
43
  from . import h3
35
44
 
36
45
  items, unconditional = h3.precache(
37
- dataset_dir, models_dir, device, dtype, resolution, flip, dropout > 0
46
+ dataset_dir, models_dir, device, dtype, resolution, flip, dropout > 0, clip_frames,
47
+ clip_window=clip_window, on_status=on_status,
38
48
  )
39
49
  return items, unconditional, _H3_SHIFT
40
50
 
@@ -105,13 +105,30 @@ def _load_with_fallback(model_id: str) -> tuple[Any, Any, Any]:
105
105
  raise first from None
106
106
 
107
107
 
108
+ def _open(path: str) -> Any:
109
+ """The frame to caption. A clip is captioned from its middle frame, which is more
110
+ representative than the first and stops PIL raising on a container it cannot read."""
111
+ from pathlib import Path
112
+
113
+ from PIL import Image
114
+
115
+ from . import dataset as ds
116
+
117
+ if not ds.is_video(Path(path)):
118
+ return Image.open(path).convert("RGB")
119
+
120
+ from ..models.minimaxh3.vendor.packing_ref2va import decode_reference_video
121
+
122
+ frames, _fps, _audio = decode_reference_video(path)
123
+ return Image.fromarray(frames[len(frames) // 2]).convert("RGB")
124
+
125
+
108
126
  def _caption_one(model: Any, processor: Any, device: str, path: str) -> str:
109
127
  """One caption. Handles both shapes: task-token models (Florence-2, which post-processes a
110
128
  tagged string) and plain image-captioning models (BLIP), which just decode the output."""
111
129
  import torch
112
- from PIL import Image
113
130
 
114
- image = Image.open(path).convert("RGB")
131
+ image = _open(path)
115
132
  task_style = hasattr(processor, "post_process_generation")
116
133
  inputs = (
117
134
  processor(text=_TASK, images=image, return_tensors="pt")
@@ -17,15 +17,25 @@ from . import arch as archs
17
17
 
18
18
  _IMAGE_SUFFIXES = (".png", ".jpg", ".jpeg", ".webp", ".bmp")
19
19
 
20
+ #: Only the video archs pass these to ``_pairs``. An image arch handed a clip would reach PIL and
21
+ #: raise, so the default stays images and each caller opts in.
22
+ _VIDEO_SUFFIXES = (".mp4", ".mov", ".webm", ".mkv", ".avi")
20
23
 
21
- def _pairs(dataset_dir: Path) -> list[tuple[Path, str]]:
24
+
25
+ def is_video(path: Path) -> bool:
26
+ return path.suffix.lower() in _VIDEO_SUFFIXES
27
+
28
+
29
+ def _pairs(
30
+ dataset_dir: Path, suffixes: tuple[str, ...] = _IMAGE_SUFFIXES
31
+ ) -> list[tuple[Path, str]]:
22
32
  out: list[tuple[Path, str]] = []
23
- for img in sorted(dataset_dir.iterdir()):
24
- if img.suffix.lower() not in _IMAGE_SUFFIXES:
33
+ for media in sorted(dataset_dir.iterdir()):
34
+ if media.suffix.lower() not in suffixes:
25
35
  continue
26
- caption_file = img.with_suffix(".txt")
36
+ caption_file = media.with_suffix(".txt")
27
37
  caption = caption_file.read_text(encoding="utf-8").strip() if caption_file.exists() else ""
28
- out.append((img, caption))
38
+ out.append((media, caption))
29
39
  return out
30
40
 
31
41