inline-core 1.2.63__tar.gz → 1.2.64__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 (256) hide show
  1. {inline_core-1.2.63 → inline_core-1.2.64}/PKG-INFO +2 -1
  2. {inline_core-1.2.63 → inline_core-1.2.64}/pyproject.toml +11 -4
  3. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/device/detect.py +49 -1
  4. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/server/__main__.py +20 -7
  5. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/studio/handlers.py +2 -0
  6. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/studio/store.py +40 -1
  7. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/studio/training.py +36 -0
  8. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/training/arch.py +16 -0
  9. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/training/cache.py +11 -2
  10. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/training/caption.py +19 -2
  11. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/training/dataset.py +15 -5
  12. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/training/h3.py +102 -27
  13. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/training/models.py +60 -0
  14. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/training/trainer.py +21 -4
  15. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_device_detect.py +69 -1
  16. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_minimaxh3_training.py +93 -0
  17. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_studio_store.py +32 -0
  18. inline_core-1.2.64/tests/test_studio_training.py +175 -0
  19. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_training_models.py +65 -0
  20. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_webui_install.py +100 -1
  21. {inline_core-1.2.63 → inline_core-1.2.64}/uv.lock +47 -85
  22. {inline_core-1.2.63 → inline_core-1.2.64}/webui.bat +53 -5
  23. {inline_core-1.2.63 → inline_core-1.2.64}/webui.sh +81 -7
  24. inline_core-1.2.63/tests/test_studio_training.py +0 -81
  25. {inline_core-1.2.63 → inline_core-1.2.64}/.gitignore +0 -0
  26. {inline_core-1.2.63 → inline_core-1.2.64}/.python-version +0 -0
  27. {inline_core-1.2.63 → inline_core-1.2.64}/CLAUDE.md +0 -0
  28. {inline_core-1.2.63 → inline_core-1.2.64}/README.md +0 -0
  29. {inline_core-1.2.63 → inline_core-1.2.64}/main.py +0 -0
  30. {inline_core-1.2.63 → inline_core-1.2.64}/scripts/flux2_train_matrix.py +0 -0
  31. {inline_core-1.2.63 → inline_core-1.2.64}/scripts/minimax_h3_lora_check.py +0 -0
  32. {inline_core-1.2.63 → inline_core-1.2.64}/scripts/minimax_h3_train_matrix.py +0 -0
  33. {inline_core-1.2.63 → inline_core-1.2.64}/scripts/reference.py +0 -0
  34. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/__init__.py +0 -0
  35. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/components/__init__.py +0 -0
  36. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/components/conditioning.py +0 -0
  37. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/components/interfaces.py +0 -0
  38. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/config.py +0 -0
  39. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/device/__init__.py +0 -0
  40. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/device/auto.py +0 -0
  41. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/device/memory.py +0 -0
  42. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/device/policy.py +0 -0
  43. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/device/types.py +0 -0
  44. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/errors.py +0 -0
  45. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/extensions/__init__.py +0 -0
  46. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/extensions/api.py +0 -0
  47. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/extensions/constraints.py +0 -0
  48. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/extensions/fetch.py +0 -0
  49. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/extensions/handlers.py +0 -0
  50. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/extensions/importer.py +0 -0
  51. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/extensions/install.py +0 -0
  52. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/extensions/loader.py +0 -0
  53. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/extensions/manifest.py +0 -0
  54. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/extensions/models.py +0 -0
  55. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/extensions/paths.py +0 -0
  56. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/extensions/resolve.py +0 -0
  57. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/extensions/scanner.py +0 -0
  58. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/extensions/state.py +0 -0
  59. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/extensions/tools.py +0 -0
  60. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/ffmpeg.py +0 -0
  61. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/graph/__init__.py +0 -0
  62. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/graph/cache.py +0 -0
  63. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/graph/descriptor.py +0 -0
  64. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/graph/executor.py +0 -0
  65. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/graph/loader_runners.py +0 -0
  66. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/graph/primitives.py +0 -0
  67. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/graph/registry.py +0 -0
  68. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/graph/runners.py +0 -0
  69. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/graph/schema.py +0 -0
  70. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/graph/topo.py +0 -0
  71. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/graph/validate.py +0 -0
  72. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/media.py +0 -0
  73. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/__init__.py +0 -0
  74. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/catalog.py +0 -0
  75. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/checkpoint.py +0 -0
  76. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/controlspace.py +0 -0
  77. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/flux2/__init__.py +0 -0
  78. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/flux2/controlnet.py +0 -0
  79. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/flux2/embeds.py +0 -0
  80. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/flux2/provider.py +0 -0
  81. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/flux2/requirements.py +0 -0
  82. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/flux2/runner.py +0 -0
  83. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/flux2/variants.py +0 -0
  84. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/keymap.py +0 -0
  85. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/krea2/__init__.py +0 -0
  86. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/krea2/convert.py +0 -0
  87. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/krea2/depth_control.py +0 -0
  88. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/krea2/img2img.py +0 -0
  89. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/krea2/provider.py +0 -0
  90. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/krea2/requirements.py +0 -0
  91. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/krea2/runner.py +0 -0
  92. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/loaders.py +0 -0
  93. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/lora.py +0 -0
  94. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/minimaxh3/__init__.py +0 -0
  95. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/minimaxh3/adaln.py +0 -0
  96. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/minimaxh3/keys.py +0 -0
  97. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/minimaxh3/load.py +0 -0
  98. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/minimaxh3/pipeline.py +0 -0
  99. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/minimaxh3/provider.py +0 -0
  100. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/minimaxh3/requirements.py +0 -0
  101. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/minimaxh3/runner.py +0 -0
  102. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/minimaxh3/vae_keys.py +0 -0
  103. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/minimaxh3/vendor/__init__.py +0 -0
  104. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/minimaxh3/vendor/autoencoder_kl_minimax_h3.py +0 -0
  105. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/minimaxh3/vendor/autoencoder_kl_minimax_h3_audio.py +0 -0
  106. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/minimaxh3/vendor/before_denoise.py +0 -0
  107. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/minimaxh3/vendor/before_encoder.py +0 -0
  108. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/minimaxh3/vendor/decoders.py +0 -0
  109. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/minimaxh3/vendor/denoise.py +0 -0
  110. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/minimaxh3/vendor/encoders.py +0 -0
  111. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/minimaxh3/vendor/modular_blocks_minimax_h3.py +0 -0
  112. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/minimaxh3/vendor/modular_pipeline.py +0 -0
  113. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/minimaxh3/vendor/packing.py +0 -0
  114. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/minimaxh3/vendor/packing_ref2va.py +0 -0
  115. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/minimaxh3/vendor/scheduling_minimax_h3.py +0 -0
  116. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/minimaxh3/vendor/transformer_minimax_h3.py +0 -0
  117. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/offload.py +0 -0
  118. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/pipeline_runtime.py +0 -0
  119. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/prepared.py +0 -0
  120. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/preprocess/__init__.py +0 -0
  121. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/preprocess/requirements.py +0 -0
  122. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/preprocess/runner.py +0 -0
  123. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/references.py +0 -0
  124. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/requirements.py +0 -0
  125. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/sampling.py +0 -0
  126. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/video_params.py +0 -0
  127. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/zimage/__init__.py +0 -0
  128. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/zimage/primitives.py +0 -0
  129. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/zimage/provider.py +0 -0
  130. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/zimage/requirements.py +0 -0
  131. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/models/zimage/runner.py +0 -0
  132. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/parallel/__init__.py +0 -0
  133. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/parallel/config.py +0 -0
  134. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/parallel/group.py +0 -0
  135. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/parallel/launch.py +0 -0
  136. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/parallel/protocol.py +0 -0
  137. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/parallel/registry.py +0 -0
  138. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/parallel/worker.py +0 -0
  139. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/runtime/__init__.py +0 -0
  140. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/runtime/context.py +0 -0
  141. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/runtime/file_store.py +0 -0
  142. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/runtime/progress.py +0 -0
  143. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/runtime/run.py +0 -0
  144. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/runtime/store.py +0 -0
  145. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/runtime/video_encode.py +0 -0
  146. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/sampling/__init__.py +0 -0
  147. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/sampling/batch.py +0 -0
  148. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/server/__init__.py +0 -0
  149. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/server/app.py +0 -0
  150. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/server/assets.py +0 -0
  151. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/server/bootstrap.py +0 -0
  152. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/server/frontend.py +0 -0
  153. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/server/manager.py +0 -0
  154. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/server/rpc.py +0 -0
  155. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/server/run_store.py +0 -0
  156. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/server/serialize.py +0 -0
  157. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/studio/__init__.py +0 -0
  158. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/studio/assets.py +0 -0
  159. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/studio/config.py +0 -0
  160. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/studio/fal.py +0 -0
  161. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/studio/frames.py +0 -0
  162. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/studio/generation.py +0 -0
  163. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/studio/graph_build.py +0 -0
  164. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/studio/image_meta.py +0 -0
  165. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/studio/models.py +0 -0
  166. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/studio/moodboard.py +0 -0
  167. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/studio/peaks.py +0 -0
  168. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/studio/recipe.py +0 -0
  169. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/studio/schema.py +0 -0
  170. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/studio/system_stats.py +0 -0
  171. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/studio/timeline/__init__.py +0 -0
  172. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/studio/timeline/compose.py +0 -0
  173. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/studio/timeline/ffmpeg.py +0 -0
  174. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/studio/timeline/render.py +0 -0
  175. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/studio/timeline/resolve.py +0 -0
  176. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/studio/training_store.py +0 -0
  177. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/takes.py +0 -0
  178. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/training/__init__.py +0 -0
  179. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/training/__main__.py +0 -0
  180. {inline_core-1.2.63 → inline_core-1.2.64}/src/inline_core/training/protocol.py +0 -0
  181. {inline_core-1.2.63 → inline_core-1.2.64}/tests/conftest.py +0 -0
  182. {inline_core-1.2.63 → inline_core-1.2.64}/tests/helpers.py +0 -0
  183. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_cache.py +0 -0
  184. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_catalog.py +0 -0
  185. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_checkpoint.py +0 -0
  186. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_config.py +0 -0
  187. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_executor.py +0 -0
  188. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_extension_api.py +0 -0
  189. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_extension_install.py +0 -0
  190. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_extension_manifest.py +0 -0
  191. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_extension_resolve.py +0 -0
  192. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_extension_scanner.py +0 -0
  193. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_extension_spine.py +0 -0
  194. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_extension_state.py +0 -0
  195. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_file_store.py +0 -0
  196. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_flux2_controlnet.py +0 -0
  197. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_flux2_folder.py +0 -0
  198. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_flux2_resolve.py +0 -0
  199. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_flux2_runner.py +0 -0
  200. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_flux2_training.py +0 -0
  201. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_flux2_variants.py +0 -0
  202. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_frontend_serving.py +0 -0
  203. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_hidden_nodes.py +0 -0
  204. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_keymap.py +0 -0
  205. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_krea2_convert.py +0 -0
  206. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_krea2_depth_control.py +0 -0
  207. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_krea2_requirements.py +0 -0
  208. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_krea2_runner.py +0 -0
  209. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_loader_runners.py +0 -0
  210. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_loaders.py +0 -0
  211. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_lora.py +0 -0
  212. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_lora_download.py +0 -0
  213. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_memory_policy.py +0 -0
  214. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_minimaxh3_adaln.py +0 -0
  215. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_minimaxh3_keys.py +0 -0
  216. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_minimaxh3_load.py +0 -0
  217. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_minimaxh3_nodes.py +0 -0
  218. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_model_requirements.py +0 -0
  219. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_offload_prepared.py +0 -0
  220. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_output_kind_contract.py +0 -0
  221. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_parallel_group.py +0 -0
  222. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_pipeline_cache.py +0 -0
  223. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_primitives.py +0 -0
  224. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_recipe.py +0 -0
  225. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_references.py +0 -0
  226. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_rpc_bridge.py +0 -0
  227. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_run_store.py +0 -0
  228. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_sampling.py +0 -0
  229. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_schema.py +0 -0
  230. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_server.py +0 -0
  231. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_staged_residency.py +0 -0
  232. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_studio_assets.py +0 -0
  233. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_studio_fal.py +0 -0
  234. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_studio_frames.py +0 -0
  235. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_studio_generation.py +0 -0
  236. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_studio_graph_build.py +0 -0
  237. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_studio_models.py +0 -0
  238. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_studio_moodboard.py +0 -0
  239. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_studio_multi_reference.py +0 -0
  240. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_studio_node_size.py +0 -0
  241. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_studio_peaks.py +0 -0
  242. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_studio_rpc.py +0 -0
  243. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_studio_schema.py +0 -0
  244. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_studio_timeline.py +0 -0
  245. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_take_bytes.py +0 -0
  246. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_topo.py +0 -0
  247. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_training_arch.py +0 -0
  248. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_training_dataset.py +0 -0
  249. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_training_resolve.py +0 -0
  250. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_validate.py +0 -0
  251. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_video_encode.py +0 -0
  252. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_video_params.py +0 -0
  253. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_xfuser_sampler.py +0 -0
  254. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_zimage_primitives.py +0 -0
  255. {inline_core-1.2.63 → inline_core-1.2.64}/tests/test_zimage_resolve.py +0 -0
  256. {inline_core-1.2.63 → inline_core-1.2.64}/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.64
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.64"
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"
@@ -76,10 +76,58 @@ def cpu_only_torch_warning() -> str | None:
76
76
  "This install has a CPU-ONLY build of PyTorch, but an NVIDIA GPU was detected. "
77
77
  "Generation will run on the CPU and be roughly 100x slower. Reinstall torch from the CUDA "
78
78
  "index, e.g. `pip install --force-reinstall --extra-index-url "
79
- "https://download.pytorch.org/whl/cu124 torch` (or re-run `webui.sh --install`)."
79
+ "https://download.pytorch.org/whl/cu126 torch` (or re-run `webui.sh --install`)."
80
80
  )
81
81
 
82
82
 
83
+ def unsupported_arch_warning() -> str | None:
84
+ """A warning when the installed torch has no kernels for the GPU it is about to run on.
85
+
86
+ An RTX 50-series card (sm_120) under a wheel built for sm_50..sm_90 is the sharpest case: torch
87
+ reports a CUDA build and a visible device, so every check in ``cpu_only_torch_warning`` passes,
88
+ and the only clue the user gets is PyTorch's own late UserWarning followed by kernels that
89
+ cannot launch. Returns None on any uncertainty, same as the rest of this module.
90
+ """
91
+ try:
92
+ import torch
93
+
94
+ if getattr(torch.version, "hip", None):
95
+ return None # the sm_ vocabulary is NVIDIA's; HIP reports gfx arches through it
96
+ get_arch_list = getattr(torch.cuda, "get_arch_list", None)
97
+ get_capability = getattr(torch.cuda, "get_device_capability", None)
98
+ device_count = getattr(torch.cuda, "device_count", None)
99
+ if get_arch_list is None or get_capability is None or device_count is None:
100
+ return None
101
+ if device_count() < 1:
102
+ return None
103
+ arches = [str(arch) for arch in get_arch_list() if str(arch).startswith("sm_")]
104
+ if not arches:
105
+ return None # a CPU-only build; cpu_only_torch_warning owns that case
106
+ major, minor = get_capability(0)
107
+ target = f"sm_{major}{minor}"
108
+ # startswith, because a wheel lists tuned variants like sm_90a for the same architecture.
109
+ if any(arch.startswith(target) for arch in arches):
110
+ return None
111
+ name = _device_name(torch) or "The detected NVIDIA GPU"
112
+ return (
113
+ f"{name} is compute capability {target}, but this install's PyTorch only has kernels "
114
+ f"for {' '.join(arches)}. Generation will fail or fall back to the CPU. Re-run "
115
+ "`webui.sh --install` (Windows: `.\\webui.bat --install`) to pick the wheel index that "
116
+ "matches the card, or force one with `--torch-index cu130` - `cu128` if the driver "
117
+ "predates CUDA 13."
118
+ )
119
+ except Exception: # noqa: BLE001 - a diagnostic must never break startup
120
+ return None
121
+
122
+
123
+ def _device_name(torch: object) -> str | None:
124
+ try:
125
+ get_name = getattr(getattr(torch, "cuda", None), "get_device_name", None)
126
+ return str(get_name(0)) if get_name is not None else None
127
+ except Exception: # noqa: BLE001
128
+ return None
129
+
130
+
83
131
  def _nvidia_gpu_present() -> bool:
84
132
  """Best-effort check for NVIDIA hardware that does NOT rely on torch's CUDA support - that is
85
133
  the whole point, since we are called precisely when torch cannot see the GPU.
@@ -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,21 @@ def build(
28
29
  *,
29
30
  flip: bool = False,
30
31
  dropout: float = 0.0,
32
+ clip_frames: int = 1,
33
+ on_status: Callable[[str], None] | None = None,
31
34
  ) -> tuple[list[dict[str, Any]], dict[str, Any] | None, float]:
32
- """Return ``(items, unconditional, shift)``, all as CPU tensors, with the encoders freed."""
35
+ """Return ``(items, unconditional, shift)``, all as CPU tensors, with the encoders freed.
36
+
37
+ ``on_status`` reports phase progress to the caller, which forwards it over the JSON protocol.
38
+ Precaching a large dataset takes minutes, and a logger call would be dropped here: the trainer
39
+ subprocess configures no logging handler, so anything below WARNING goes nowhere.
40
+ """
33
41
  if arch == archs.MINIMAX_H3:
34
42
  from . import h3
35
43
 
36
44
  items, unconditional = h3.precache(
37
- dataset_dir, models_dir, device, dtype, resolution, flip, dropout > 0
45
+ dataset_dir, models_dir, device, dtype, resolution, flip, dropout > 0, clip_frames,
46
+ on_status=on_status,
38
47
  )
39
48
  return items, unconditional, _H3_SHIFT
40
49
 
@@ -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
 
@@ -14,6 +14,7 @@ from __future__ import annotations
14
14
 
15
15
  import gc
16
16
  import logging
17
+ from collections.abc import Callable
17
18
  from pathlib import Path
18
19
  from typing import Any
19
20
 
@@ -29,6 +30,10 @@ AUDIO_LATENT_CHANNELS = 32
29
30
  #: caption dropout and an image whose ``.txt`` is missing.
30
31
  _EMPTY_CAPTION = " "
31
32
 
33
+ #: H3's fixed frame rate, and the shortest clip its video VAE encodes (the first ``17n + 5``).
34
+ _H3_FPS = 24
35
+ _MIN_CLIP_FRAMES = 22
36
+
32
37
 
33
38
  def precache(
34
39
  dataset_dir: str,
@@ -38,38 +43,55 @@ def precache(
38
43
  resolution: int,
39
44
  flip: bool,
40
45
  want_unconditional: bool,
46
+ clip_frames: int = 1,
47
+ on_status: Callable[[str], None] | None = None,
41
48
  ) -> tuple[list[dict[str, Any]], dict[str, Any] | None]:
42
49
  """Every image as a latent and every caption as conditioning, as CPU tensors."""
43
50
  from . import dataset as ds
44
51
 
45
- pairs = ds._pairs(Path(dataset_dir))
52
+ say = on_status or (lambda _text: None)
53
+ pairs = ds._pairs(Path(dataset_dir), ds._IMAGE_SUFFIXES + ds._VIDEO_SUFFIXES)
46
54
  if not pairs:
47
55
  raise RuntimeError("The exported dataset is empty.")
48
56
 
49
57
  root = Path(models_dir)
50
- latents = _encode_pixels(root, pairs, device, resolution, flip)
51
- captions = [caption for _img, caption in pairs for _ in ((False, True) if flip else (False,))]
58
+ # Only the clips that survived encoding carry captions, or every caption after the first skip
59
+ # would be paired with the wrong latent.
60
+ latents, kept = _encode_pixels(root, pairs, device, resolution, flip, clip_frames, say)
61
+ if not kept:
62
+ raise RuntimeError(
63
+ f"None of the {len(pairs)} dataset items could be encoded. For clips, each must be at "
64
+ f"least {_MIN_CLIP_FRAMES} frames at {_H3_FPS}fps "
65
+ f"({_MIN_CLIP_FRAMES / _H3_FPS:.2f}s)."
66
+ )
67
+ captions = [caption for _img, caption in kept for _ in ((False, True) if flip else (False,))]
52
68
  if want_unconditional:
53
69
  captions.append("")
54
- embeds = _encode_captions(root, captions, device, dtype)
70
+ embeds = _encode_captions(root, captions, device, dtype, say)
71
+ say(f"cached {len(latents)} latents and {len(embeds)} captions")
55
72
 
56
73
  items = [
57
74
  {"latent": latent, **_conditioning(embed, tags, latent)}
58
75
  for latent, (embed, tags) in zip(latents, embeds, strict=False)
59
76
  ]
60
- unconditional = None
61
77
  if want_unconditional:
78
+ # Dropout swaps a different text length in, which moves every row after it, so the whole
79
+ # layout travels with the embedding. It also depends on the latent grid, and a dataset
80
+ # mixing stills with clips has more than one, so each item carries its own rather than
81
+ # sharing the first item's and mis-sizing every clip.
62
82
  embed, tags = embeds[-1]
63
- # Caption dropout swaps in a different text length, which moves every row after it, so the
64
- # whole layout travels with the embedding rather than just the embedding.
65
- unconditional = _conditioning(embed, tags, latents[0])
66
- return items, unconditional
83
+ for item in items:
84
+ item["uncond"] = _conditioning(embed, tags, item["latent"])
85
+ # The per-item copies are what dropout uses; the loop keeps the global slot for the image archs.
86
+ return items, None
67
87
 
68
88
 
69
89
  def _encode_pixels(
70
- root: Path, pairs: list[tuple[Path, str]], device: str, resolution: int, flip: bool
71
- ) -> list[Any]:
72
- """Pass one: the video VAE, then dropped."""
90
+ root: Path, pairs: list[tuple[Path, str]], device: str, resolution: int, flip: bool,
91
+ clip_frames: int = 1, say: Callable[[str], None] = lambda _text: None,
92
+ ) -> tuple[list[Any], list[tuple[Path, str]]]:
93
+ """Pass one: the video VAE, then dropped. Returns the latents and the pairs they came from."""
94
+ import numpy
73
95
  import torch
74
96
  from PIL import Image
75
97
 
@@ -84,45 +106,98 @@ def _encode_pixels(
84
106
  from . import dataset as ds
85
107
 
86
108
  out: list[Any] = []
109
+ kept: list[tuple[Path, str]] = []
110
+ skipped: list[str] = []
111
+ total = len(pairs)
112
+ say(f"encoding {total} items at {resolution}px through the video VAE")
87
113
  try:
88
- for img_path, _caption in pairs:
114
+ for index, (path, _caption) in enumerate(pairs, start=1):
115
+ clip = ds.is_video(path)
116
+ try:
117
+ frames = _clip_frames(path, clip_frames) if clip else [Image.open(path)]
118
+ except ShortClipError as exc:
119
+ skipped.append(path.name)
120
+ say(f"skipped {exc}")
121
+ continue
122
+ # A long precache is otherwise silent for many minutes, so report often enough that it
123
+ # reads as progress rather than a hang.
124
+ if index == 1 or index % 5 == 0 or index == total:
125
+ say(f"caching latents {index}/{total}")
89
126
  for mirrored in (False, True) if flip else (False,):
90
- square = ds._square(Image.open(img_path), resolution, mirrored)
91
- # H3 normalises with ImageNet statistics, not to [-1, 1] like the image archs, and
92
- # a still is one frame: (1, 3, 1, H, W).
93
- raw = torch.from_numpy(_as_array(square)).to(device)
94
- pixels = raw.permute(2, 0, 1)[None, :, None]
127
+ stack = [_as_array(ds._square(f, resolution, mirrored)) for f in frames]
128
+ # ImageNet statistics, not the [-1, 1] the image archs use, and always 5D:
129
+ # (1, 3, F, H, W).
130
+ raw = torch.from_numpy(numpy.stack(stack)).to(device)
131
+ pixels = raw.permute(3, 0, 1, 2)[None]
95
132
  pixels = (pixels.to(torch.float32).div(255.0) - pixel_mean) / pixel_std
96
133
  with torch.no_grad():
97
- # The spatial encoder alone, the path inference uses for a single frame; the
98
- # temporal chunking is for 17n+5 clips.
99
- latent = _sample(vae._encode_clip(pixels))
134
+ # A single frame takes the spatial encoder; a 17n+5 clip takes the temporal
135
+ # chunking. Mirrors the split the vendored reference encoder makes.
136
+ moments = vae._encode(pixels) if clip else vae._encode_clip(pixels)
137
+ latent = _sample(moments)
100
138
  out.append(((latent.cpu() - mean) / std)[0])
139
+ kept.append((path, _caption))
101
140
  finally:
102
141
  del vae
103
142
  _reclaim()
104
- logger.info("MiniMax H3: cached %d latents, video VAE released", len(out))
105
- return out
143
+ if skipped:
144
+ say(f"skipped {len(skipped)} of {total} items as too short: {', '.join(skipped)}")
145
+ say(f"cached {len(out)} latents from {len(kept)} items, video VAE released")
146
+ return out, kept
147
+
148
+
149
+ class ShortClipError(RuntimeError):
150
+ """A clip below H3's frame floor. Skipped, never fatal: one bad file in a large dataset must
151
+ not throw away a precache that takes many minutes."""
152
+
153
+
154
+ def _clip_frames(path: Path, clip_frames: int) -> list[Any]:
155
+ """A clip as PIL frames on H3's 24fps, 17n+5 grid, taken from the start.
156
+
157
+ Trimmed rather than sampled: a fixed window keeps the precache to one encode per clip, and
158
+ re-encoding a different window every step would defeat caching the latents at all.
159
+ """
160
+ from PIL import Image
161
+
162
+ from ..models.minimaxh3.vendor.packing_ref2va import (
163
+ decode_reference_video,
164
+ resample_reference_frames,
165
+ trim_reference_num_frames,
166
+ )
167
+
168
+ decoded, fps, _audio = decode_reference_video(str(path))
169
+ frames = resample_reference_frames(decoded, fps)
170
+ keep = trim_reference_num_frames(min(frames.shape[0], clip_frames))
171
+ if keep > frames.shape[0]:
172
+ raise ShortClipError(
173
+ f"{path.name} is {frames.shape[0]} frames once resampled to {_H3_FPS}fps, below H3's "
174
+ f"{keep}-frame minimum ({keep / _H3_FPS:.2f}s). Skipped."
175
+ )
176
+ return [Image.fromarray(frame) for frame in frames[:keep]]
106
177
 
107
178
 
108
179
  def _encode_captions(
109
- root: Path, captions: list[str], device: str, dtype: Any
180
+ root: Path, captions: list[str], device: str, dtype: Any,
181
+ say: Callable[[str], None] = lambda _text: None,
110
182
  ) -> list[tuple[Any, Any]]:
111
183
  """Pass two: the 4-bit conditioner, then dropped."""
112
184
  import torch
113
185
 
114
186
  from ..models.minimaxh3.vendor.encoders import MiniMaxH3TextEncoderStep
115
187
 
188
+ say("loading the 4-bit text conditioner (20.5GB)")
116
189
  pipeline = _load_conditioner(root, device, dtype)
117
190
  # Encode wherever it landed. It spills to host RAM on a card too small for 20.5GB, and the
118
191
  # vendored step builds its input ids on the device it is handed, so CUDA ids against a
119
192
  # CPU-resident encoder fail in `index_select`.
120
193
  where = next(pipeline.text_encoder.parameters()).device
121
194
  if where.type != torch.device(device).type:
122
- logger.info("MiniMax H3: conditioner is on %s, encoding captions there", where)
195
+ say(f"conditioner spilled to {where}, encoding captions there (slower)")
123
196
  out: list[tuple[Any, Any]] = []
124
197
  try:
125
- for caption in captions:
198
+ for index, caption in enumerate(captions, start=1):
199
+ if index == 1 or index % 10 == 0 or index == len(captions):
200
+ say(f"encoding captions {index}/{len(captions)}")
126
201
  caption = caption or _EMPTY_CAPTION
127
202
  with torch.no_grad():
128
203
  # The staticmethod rather than the block, so nothing needs a PipelineState. `dtype`
@@ -134,7 +209,7 @@ def _encode_captions(
134
209
  out.append((embeds[0].cpu(), tags.cpu()))
135
210
  finally:
136
211
  _drop_conditioner(pipeline)
137
- logger.info("MiniMax H3: cached %d captions, conditioner released", len(out))
212
+ say(f"cached {len(out)} captions, conditioner released")
138
213
  return out
139
214
 
140
215