inline-core 1.2.64__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 (256) hide show
  1. {inline_core-1.2.64 → inline_core-1.2.65}/PKG-INFO +1 -1
  2. {inline_core-1.2.64 → inline_core-1.2.65}/pyproject.toml +1 -1
  3. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/device/detect.py +37 -4
  4. inline_core-1.2.65/src/inline_core/device/probe.py +76 -0
  5. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/minimaxh3/load.py +8 -0
  6. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/training/cache.py +2 -1
  7. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/training/h3.py +15 -5
  8. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/training/trainer.py +1 -0
  9. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_device_detect.py +129 -0
  10. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_minimaxh3_training.py +23 -0
  11. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_webui_install.py +88 -3
  12. {inline_core-1.2.64 → inline_core-1.2.65}/webui.bat +155 -18
  13. {inline_core-1.2.64 → inline_core-1.2.65}/webui.sh +165 -17
  14. {inline_core-1.2.64 → inline_core-1.2.65}/.gitignore +0 -0
  15. {inline_core-1.2.64 → inline_core-1.2.65}/.python-version +0 -0
  16. {inline_core-1.2.64 → inline_core-1.2.65}/CLAUDE.md +0 -0
  17. {inline_core-1.2.64 → inline_core-1.2.65}/README.md +0 -0
  18. {inline_core-1.2.64 → inline_core-1.2.65}/main.py +0 -0
  19. {inline_core-1.2.64 → inline_core-1.2.65}/scripts/flux2_train_matrix.py +0 -0
  20. {inline_core-1.2.64 → inline_core-1.2.65}/scripts/minimax_h3_lora_check.py +0 -0
  21. {inline_core-1.2.64 → inline_core-1.2.65}/scripts/minimax_h3_train_matrix.py +0 -0
  22. {inline_core-1.2.64 → inline_core-1.2.65}/scripts/reference.py +0 -0
  23. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/__init__.py +0 -0
  24. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/components/__init__.py +0 -0
  25. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/components/conditioning.py +0 -0
  26. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/components/interfaces.py +0 -0
  27. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/config.py +0 -0
  28. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/device/__init__.py +0 -0
  29. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/device/auto.py +0 -0
  30. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/device/memory.py +0 -0
  31. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/device/policy.py +0 -0
  32. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/device/types.py +0 -0
  33. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/errors.py +0 -0
  34. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/extensions/__init__.py +0 -0
  35. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/extensions/api.py +0 -0
  36. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/extensions/constraints.py +0 -0
  37. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/extensions/fetch.py +0 -0
  38. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/extensions/handlers.py +0 -0
  39. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/extensions/importer.py +0 -0
  40. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/extensions/install.py +0 -0
  41. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/extensions/loader.py +0 -0
  42. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/extensions/manifest.py +0 -0
  43. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/extensions/models.py +0 -0
  44. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/extensions/paths.py +0 -0
  45. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/extensions/resolve.py +0 -0
  46. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/extensions/scanner.py +0 -0
  47. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/extensions/state.py +0 -0
  48. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/extensions/tools.py +0 -0
  49. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/ffmpeg.py +0 -0
  50. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/graph/__init__.py +0 -0
  51. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/graph/cache.py +0 -0
  52. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/graph/descriptor.py +0 -0
  53. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/graph/executor.py +0 -0
  54. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/graph/loader_runners.py +0 -0
  55. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/graph/primitives.py +0 -0
  56. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/graph/registry.py +0 -0
  57. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/graph/runners.py +0 -0
  58. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/graph/schema.py +0 -0
  59. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/graph/topo.py +0 -0
  60. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/graph/validate.py +0 -0
  61. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/media.py +0 -0
  62. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/__init__.py +0 -0
  63. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/catalog.py +0 -0
  64. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/checkpoint.py +0 -0
  65. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/controlspace.py +0 -0
  66. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/flux2/__init__.py +0 -0
  67. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/flux2/controlnet.py +0 -0
  68. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/flux2/embeds.py +0 -0
  69. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/flux2/provider.py +0 -0
  70. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/flux2/requirements.py +0 -0
  71. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/flux2/runner.py +0 -0
  72. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/flux2/variants.py +0 -0
  73. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/keymap.py +0 -0
  74. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/krea2/__init__.py +0 -0
  75. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/krea2/convert.py +0 -0
  76. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/krea2/depth_control.py +0 -0
  77. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/krea2/img2img.py +0 -0
  78. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/krea2/provider.py +0 -0
  79. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/krea2/requirements.py +0 -0
  80. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/krea2/runner.py +0 -0
  81. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/loaders.py +0 -0
  82. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/lora.py +0 -0
  83. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/minimaxh3/__init__.py +0 -0
  84. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/minimaxh3/adaln.py +0 -0
  85. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/minimaxh3/keys.py +0 -0
  86. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/minimaxh3/pipeline.py +0 -0
  87. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/minimaxh3/provider.py +0 -0
  88. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/minimaxh3/requirements.py +0 -0
  89. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/minimaxh3/runner.py +0 -0
  90. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/minimaxh3/vae_keys.py +0 -0
  91. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/minimaxh3/vendor/__init__.py +0 -0
  92. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/minimaxh3/vendor/autoencoder_kl_minimax_h3.py +0 -0
  93. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/minimaxh3/vendor/autoencoder_kl_minimax_h3_audio.py +0 -0
  94. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/minimaxh3/vendor/before_denoise.py +0 -0
  95. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/minimaxh3/vendor/before_encoder.py +0 -0
  96. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/minimaxh3/vendor/decoders.py +0 -0
  97. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/minimaxh3/vendor/denoise.py +0 -0
  98. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/minimaxh3/vendor/encoders.py +0 -0
  99. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/minimaxh3/vendor/modular_blocks_minimax_h3.py +0 -0
  100. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/minimaxh3/vendor/modular_pipeline.py +0 -0
  101. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/minimaxh3/vendor/packing.py +0 -0
  102. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/minimaxh3/vendor/packing_ref2va.py +0 -0
  103. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/minimaxh3/vendor/scheduling_minimax_h3.py +0 -0
  104. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/minimaxh3/vendor/transformer_minimax_h3.py +0 -0
  105. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/offload.py +0 -0
  106. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/pipeline_runtime.py +0 -0
  107. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/prepared.py +0 -0
  108. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/preprocess/__init__.py +0 -0
  109. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/preprocess/requirements.py +0 -0
  110. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/preprocess/runner.py +0 -0
  111. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/references.py +0 -0
  112. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/requirements.py +0 -0
  113. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/sampling.py +0 -0
  114. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/video_params.py +0 -0
  115. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/zimage/__init__.py +0 -0
  116. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/zimage/primitives.py +0 -0
  117. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/zimage/provider.py +0 -0
  118. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/zimage/requirements.py +0 -0
  119. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/models/zimage/runner.py +0 -0
  120. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/parallel/__init__.py +0 -0
  121. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/parallel/config.py +0 -0
  122. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/parallel/group.py +0 -0
  123. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/parallel/launch.py +0 -0
  124. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/parallel/protocol.py +0 -0
  125. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/parallel/registry.py +0 -0
  126. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/parallel/worker.py +0 -0
  127. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/runtime/__init__.py +0 -0
  128. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/runtime/context.py +0 -0
  129. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/runtime/file_store.py +0 -0
  130. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/runtime/progress.py +0 -0
  131. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/runtime/run.py +0 -0
  132. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/runtime/store.py +0 -0
  133. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/runtime/video_encode.py +0 -0
  134. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/sampling/__init__.py +0 -0
  135. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/sampling/batch.py +0 -0
  136. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/server/__init__.py +0 -0
  137. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/server/__main__.py +0 -0
  138. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/server/app.py +0 -0
  139. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/server/assets.py +0 -0
  140. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/server/bootstrap.py +0 -0
  141. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/server/frontend.py +0 -0
  142. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/server/manager.py +0 -0
  143. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/server/rpc.py +0 -0
  144. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/server/run_store.py +0 -0
  145. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/server/serialize.py +0 -0
  146. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/studio/__init__.py +0 -0
  147. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/studio/assets.py +0 -0
  148. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/studio/config.py +0 -0
  149. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/studio/fal.py +0 -0
  150. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/studio/frames.py +0 -0
  151. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/studio/generation.py +0 -0
  152. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/studio/graph_build.py +0 -0
  153. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/studio/handlers.py +0 -0
  154. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/studio/image_meta.py +0 -0
  155. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/studio/models.py +0 -0
  156. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/studio/moodboard.py +0 -0
  157. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/studio/peaks.py +0 -0
  158. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/studio/recipe.py +0 -0
  159. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/studio/schema.py +0 -0
  160. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/studio/store.py +0 -0
  161. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/studio/system_stats.py +0 -0
  162. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/studio/timeline/__init__.py +0 -0
  163. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/studio/timeline/compose.py +0 -0
  164. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/studio/timeline/ffmpeg.py +0 -0
  165. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/studio/timeline/render.py +0 -0
  166. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/studio/timeline/resolve.py +0 -0
  167. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/studio/training.py +0 -0
  168. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/studio/training_store.py +0 -0
  169. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/takes.py +0 -0
  170. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/training/__init__.py +0 -0
  171. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/training/__main__.py +0 -0
  172. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/training/arch.py +0 -0
  173. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/training/caption.py +0 -0
  174. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/training/dataset.py +0 -0
  175. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/training/models.py +0 -0
  176. {inline_core-1.2.64 → inline_core-1.2.65}/src/inline_core/training/protocol.py +0 -0
  177. {inline_core-1.2.64 → inline_core-1.2.65}/tests/conftest.py +0 -0
  178. {inline_core-1.2.64 → inline_core-1.2.65}/tests/helpers.py +0 -0
  179. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_cache.py +0 -0
  180. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_catalog.py +0 -0
  181. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_checkpoint.py +0 -0
  182. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_config.py +0 -0
  183. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_executor.py +0 -0
  184. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_extension_api.py +0 -0
  185. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_extension_install.py +0 -0
  186. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_extension_manifest.py +0 -0
  187. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_extension_resolve.py +0 -0
  188. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_extension_scanner.py +0 -0
  189. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_extension_spine.py +0 -0
  190. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_extension_state.py +0 -0
  191. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_file_store.py +0 -0
  192. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_flux2_controlnet.py +0 -0
  193. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_flux2_folder.py +0 -0
  194. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_flux2_resolve.py +0 -0
  195. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_flux2_runner.py +0 -0
  196. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_flux2_training.py +0 -0
  197. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_flux2_variants.py +0 -0
  198. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_frontend_serving.py +0 -0
  199. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_hidden_nodes.py +0 -0
  200. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_keymap.py +0 -0
  201. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_krea2_convert.py +0 -0
  202. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_krea2_depth_control.py +0 -0
  203. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_krea2_requirements.py +0 -0
  204. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_krea2_runner.py +0 -0
  205. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_loader_runners.py +0 -0
  206. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_loaders.py +0 -0
  207. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_lora.py +0 -0
  208. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_lora_download.py +0 -0
  209. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_memory_policy.py +0 -0
  210. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_minimaxh3_adaln.py +0 -0
  211. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_minimaxh3_keys.py +0 -0
  212. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_minimaxh3_load.py +0 -0
  213. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_minimaxh3_nodes.py +0 -0
  214. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_model_requirements.py +0 -0
  215. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_offload_prepared.py +0 -0
  216. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_output_kind_contract.py +0 -0
  217. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_parallel_group.py +0 -0
  218. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_pipeline_cache.py +0 -0
  219. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_primitives.py +0 -0
  220. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_recipe.py +0 -0
  221. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_references.py +0 -0
  222. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_rpc_bridge.py +0 -0
  223. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_run_store.py +0 -0
  224. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_sampling.py +0 -0
  225. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_schema.py +0 -0
  226. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_server.py +0 -0
  227. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_staged_residency.py +0 -0
  228. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_studio_assets.py +0 -0
  229. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_studio_fal.py +0 -0
  230. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_studio_frames.py +0 -0
  231. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_studio_generation.py +0 -0
  232. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_studio_graph_build.py +0 -0
  233. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_studio_models.py +0 -0
  234. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_studio_moodboard.py +0 -0
  235. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_studio_multi_reference.py +0 -0
  236. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_studio_node_size.py +0 -0
  237. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_studio_peaks.py +0 -0
  238. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_studio_rpc.py +0 -0
  239. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_studio_schema.py +0 -0
  240. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_studio_store.py +0 -0
  241. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_studio_timeline.py +0 -0
  242. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_studio_training.py +0 -0
  243. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_take_bytes.py +0 -0
  244. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_topo.py +0 -0
  245. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_training_arch.py +0 -0
  246. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_training_dataset.py +0 -0
  247. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_training_models.py +0 -0
  248. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_training_resolve.py +0 -0
  249. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_validate.py +0 -0
  250. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_video_encode.py +0 -0
  251. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_video_params.py +0 -0
  252. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_xfuser_sampler.py +0 -0
  253. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_zimage_primitives.py +0 -0
  254. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_zimage_resolve.py +0 -0
  255. {inline_core-1.2.64 → inline_core-1.2.65}/tests/test_zimage_runner.py +0 -0
  256. {inline_core-1.2.64 → inline_core-1.2.65}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: inline-core
3
- Version: 1.2.64
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
@@ -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.64"
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"
@@ -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
 
@@ -80,6 +83,37 @@ def cpu_only_torch_warning() -> str | None:
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
+
83
117
  def unsupported_arch_warning() -> str | None:
84
118
  """A warning when the installed torch has no kernels for the GPU it is about to run on.
85
119
 
@@ -105,16 +139,15 @@ def unsupported_arch_warning() -> str | None:
105
139
  return None # a CPU-only build; cpu_only_torch_warning owns that case
106
140
  major, minor = get_capability(0)
107
141
  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):
142
+ if arch_list_covers(arches, major, minor):
110
143
  return None
111
144
  name = _device_name(torch) or "The detected NVIDIA GPU"
112
145
  return (
113
146
  f"{name} is compute capability {target}, but this install's PyTorch only has kernels "
114
147
  f"for {' '.join(arches)}. Generation will fail or fall back to the CPU. Re-run "
115
148
  "`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."
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."
118
151
  )
119
152
  except Exception: # noqa: BLE001 - a diagnostic must never break startup
120
153
  return None
@@ -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
@@ -30,6 +30,7 @@ def build(
30
30
  flip: bool = False,
31
31
  dropout: float = 0.0,
32
32
  clip_frames: int = 1,
33
+ clip_window: str = "start",
33
34
  on_status: Callable[[str], None] | None = None,
34
35
  ) -> tuple[list[dict[str, Any]], dict[str, Any] | None, float]:
35
36
  """Return ``(items, unconditional, shift)``, all as CPU tensors, with the encoders freed.
@@ -43,7 +44,7 @@ def build(
43
44
 
44
45
  items, unconditional = h3.precache(
45
46
  dataset_dir, models_dir, device, dtype, resolution, flip, dropout > 0, clip_frames,
46
- on_status=on_status,
47
+ clip_window=clip_window, on_status=on_status,
47
48
  )
48
49
  return items, unconditional, _H3_SHIFT
49
50
 
@@ -44,6 +44,7 @@ def precache(
44
44
  flip: bool,
45
45
  want_unconditional: bool,
46
46
  clip_frames: int = 1,
47
+ clip_window: str = "start",
47
48
  on_status: Callable[[str], None] | None = None,
48
49
  ) -> tuple[list[dict[str, Any]], dict[str, Any] | None]:
49
50
  """Every image as a latent and every caption as conditioning, as CPU tensors."""
@@ -57,7 +58,9 @@ def precache(
57
58
  root = Path(models_dir)
58
59
  # Only the clips that survived encoding carry captions, or every caption after the first skip
59
60
  # would be paired with the wrong latent.
60
- latents, kept = _encode_pixels(root, pairs, device, resolution, flip, clip_frames, say)
61
+ latents, kept = _encode_pixels(
62
+ root, pairs, device, resolution, flip, clip_frames, say, clip_window
63
+ )
61
64
  if not kept:
62
65
  raise RuntimeError(
63
66
  f"None of the {len(pairs)} dataset items could be encoded. For clips, each must be at "
@@ -89,6 +92,7 @@ def precache(
89
92
  def _encode_pixels(
90
93
  root: Path, pairs: list[tuple[Path, str]], device: str, resolution: int, flip: bool,
91
94
  clip_frames: int = 1, say: Callable[[str], None] = lambda _text: None,
95
+ clip_window: str = "start",
92
96
  ) -> tuple[list[Any], list[tuple[Path, str]]]:
93
97
  """Pass one: the video VAE, then dropped. Returns the latents and the pairs they came from."""
94
98
  import numpy
@@ -114,7 +118,9 @@ def _encode_pixels(
114
118
  for index, (path, _caption) in enumerate(pairs, start=1):
115
119
  clip = ds.is_video(path)
116
120
  try:
117
- frames = _clip_frames(path, clip_frames) if clip else [Image.open(path)]
121
+ frames = (
122
+ _clip_frames(path, clip_frames, clip_window) if clip else [Image.open(path)]
123
+ )
118
124
  except ShortClipError as exc:
119
125
  skipped.append(path.name)
120
126
  say(f"skipped {exc}")
@@ -151,11 +157,14 @@ class ShortClipError(RuntimeError):
151
157
  not throw away a precache that takes many minutes."""
152
158
 
153
159
 
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.
160
+ def _clip_frames(path: Path, clip_frames: int, window: str = "start") -> list[Any]:
161
+ """A clip as PIL frames on H3's 24fps, 17n+5 grid.
156
162
 
157
163
  Trimmed rather than sampled: a fixed window keeps the precache to one encode per clip, and
158
164
  re-encoding a different window every step would defeat caching the latents at all.
165
+
166
+ The grid only snaps **down**, so some of every clip is always dropped. ``window="end"`` takes
167
+ the tail instead of the head, for footage whose action is at the finish.
159
168
  """
160
169
  from PIL import Image
161
170
 
@@ -173,7 +182,8 @@ def _clip_frames(path: Path, clip_frames: int) -> list[Any]:
173
182
  f"{path.name} is {frames.shape[0]} frames once resampled to {_H3_FPS}fps, below H3's "
174
183
  f"{keep}-frame minimum ({keep / _H3_FPS:.2f}s). Skipped."
175
184
  )
176
- return [Image.fromarray(frame) for frame in frames[:keep]]
185
+ kept = frames[-keep:] if window == "end" else frames[:keep]
186
+ return [Image.fromarray(frame) for frame in kept]
177
187
 
178
188
 
179
189
  def _encode_captions(
@@ -184,6 +184,7 @@ def train(manifest: dict[str, Any]) -> str | None:
184
184
  manifest["datasetDir"], manifest["modelsDir"], arch.key, str(device), dtype, resolution,
185
185
  flip=bool(hp.get("flipAugment")), dropout=dropout,
186
186
  clip_frames=archs.clip_frames(arch, hp.get("clipSeconds")),
187
+ clip_window=str(hp.get("clipWindow") or "start"),
187
188
  on_status=lambda text: protocol.progress(0, steps, status=text),
188
189
  )
189
190
 
@@ -192,3 +192,132 @@ def test_silent_without_torch(monkeypatch) -> None:
192
192
  monkeypatch.setattr(builtins, "__import__", _no_torch)
193
193
  monkeypatch.delitem(__import__("sys").modules, "torch", raising=False)
194
194
  assert detect.cpu_only_torch_warning() is None
195
+
196
+
197
+ # --- CUDA within-major binary compatibility -----------------------------------------------------
198
+ #
199
+ # A cubin built for sm_8x runs on any sm_8y where y >= x, so sm_86 covers an sm_89 Ada card. Exact
200
+ # per-minor matching told every RTX 40-series owner their install was broken.
201
+
202
+ _CU130_ARCHES = ["sm_75", "sm_80", "sm_86", "sm_90", "sm_100", "sm_120"]
203
+
204
+
205
+ def test_arch_parse_reads_the_minor_as_the_last_digit() -> None:
206
+ """sm_120 is (12, 0), not (1, 20). Left-to-right puts the bug on the Blackwell parts."""
207
+ assert detect._parse_arch("sm_120") == (12, 0)
208
+ assert detect._parse_arch("sm_100") == (10, 0)
209
+ assert detect._parse_arch("sm_90a") == (9, 0) # tuned variant
210
+ assert detect._parse_arch("compute_90") is None
211
+
212
+
213
+ def test_ada_is_covered_by_ampere_kernels(monkeypatch) -> None:
214
+ """The RTX 4080 false positive: sm_89 against a wheel whose newest 8.x is sm_86."""
215
+ monkeypatch.setitem(
216
+ __import__("sys").modules,
217
+ "torch",
218
+ _fake_arch_torch(capability=(8, 9), arches=_CU124_ARCHES),
219
+ )
220
+ assert detect.unsupported_arch_warning() is None
221
+
222
+
223
+ def test_compatibility_runs_upward_only(monkeypatch) -> None:
224
+ """An sm_86 cubin does NOT run on an sm_80 A100, so that must still warn."""
225
+ monkeypatch.setitem(
226
+ __import__("sys").modules,
227
+ "torch",
228
+ _fake_arch_torch(capability=(8, 0), arches=["sm_86"]),
229
+ )
230
+ assert detect.unsupported_arch_warning() is not None
231
+
232
+
233
+ def test_a_dropped_architecture_still_warns(monkeypatch) -> None:
234
+ """cu130 dropped Volta; sm_70 has no same-major kernel at or below it."""
235
+ monkeypatch.setitem(
236
+ __import__("sys").modules,
237
+ "torch",
238
+ _fake_arch_torch(capability=(7, 0), arches=_CU130_ARCHES),
239
+ )
240
+ assert detect.unsupported_arch_warning() is not None
241
+
242
+
243
+ def test_blackwell_majors_do_not_cover_each_other() -> None:
244
+ """sm_100 and sm_120 are both Blackwell but different majors, so neither covers the other."""
245
+ assert detect.arch_list_covers(["sm_100"], 12, 0) is False
246
+ assert detect.arch_list_covers(["sm_120"], 10, 0) is False
247
+ assert detect.arch_list_covers(_CU130_ARCHES, 10, 0) is True
248
+ assert detect.arch_list_covers(_CU130_ARCHES, 12, 0) is True
249
+
250
+
251
+ # --- the install-time probe --------------------------------------------------------------------
252
+
253
+
254
+ def _fake_probe_torch(
255
+ *, version: str, arches: list[str], capability: tuple[int, int] | None = (12, 0),
256
+ hip: str | None = None, devices: int = 1,
257
+ ) -> types.SimpleNamespace:
258
+ return types.SimpleNamespace(
259
+ __version__=version,
260
+ version=types.SimpleNamespace(cuda=None if "+cpu" in version else "12.8", hip=hip),
261
+ cuda=types.SimpleNamespace(
262
+ device_count=lambda: devices,
263
+ get_arch_list=lambda: arches,
264
+ get_device_capability=lambda i=0: capability,
265
+ ),
266
+ )
267
+
268
+
269
+ def _probe_with(monkeypatch, torch_stub) -> dict:
270
+ from inline_core.device import probe as probe_mod
271
+
272
+ monkeypatch.setitem(__import__("sys").modules, "torch", torch_stub)
273
+ return probe_mod.probe()
274
+
275
+
276
+ def test_probe_reports_an_uncovered_wheel_as_replaceable(monkeypatch) -> None:
277
+ """The 5060 Ti case: a cu126 wheel on sm_120, and the +cuXXX tag makes it safe to replace."""
278
+ got = _probe_with(
279
+ monkeypatch, _fake_probe_torch(version="2.9.0+cu126", arches=_CU124_ARCHES)
280
+ )
281
+ assert got["status"] == "uncovered"
282
+ assert got["replaceable"] is True
283
+
284
+
285
+ def test_probe_reports_ada_as_covered(monkeypatch) -> None:
286
+ got = _probe_with(
287
+ monkeypatch,
288
+ _fake_probe_torch(version="2.13.0+cu130", arches=_CU130_ARCHES, capability=(8, 9)),
289
+ )
290
+ assert got["status"] == "covered"
291
+
292
+
293
+ def test_probe_never_offers_to_replace_a_rocm_build(monkeypatch) -> None:
294
+ """The safety gate. A ROCm build fails the sm_ rule too, and reinstalling over someone's
295
+ deliberate choice is worse than the wrong wheel."""
296
+ got = _probe_with(
297
+ monkeypatch,
298
+ _fake_probe_torch(version="2.9.0+rocm6.2", arches=[], hip="6.2.0"),
299
+ )
300
+ assert got["status"] == "rocm"
301
+ assert got["replaceable"] is False
302
+
303
+
304
+ def test_probe_never_offers_to_replace_an_untagged_build(monkeypatch) -> None:
305
+ """A nightly or hand-built wheel carries no +cpu/+cuXXX tag, so leave it alone."""
306
+ got = _probe_with(
307
+ monkeypatch, _fake_probe_torch(version="2.14.0.dev20260101", arches=_CU124_ARCHES)
308
+ )
309
+ assert got["status"] == "uncovered"
310
+ assert got["replaceable"] is False
311
+
312
+
313
+ def test_probe_survives_a_broken_torch(monkeypatch) -> None:
314
+ """Never covered on uncertainty: a torch that raises must read as unknown."""
315
+ class Boom:
316
+ __version__ = "2.13.0+cu130"
317
+
318
+ def __getattr__(self, _name: str):
319
+ raise RuntimeError("broken install")
320
+
321
+ got = _probe_with(monkeypatch, Boom())
322
+ assert got["status"] == "unknown"
323
+ assert got["status"] != "covered"
@@ -452,3 +452,26 @@ def test_long_enough_clip_encodes_on_the_frame_grid(tmp_path) -> None:
452
452
  frames = h3._clip_frames(clip, clip_frames=24)
453
453
  # Snapped down onto H3's 17n+5 grid rather than taking all 40.
454
454
  assert len(frames) == 22
455
+
456
+
457
+ def test_clip_window_takes_the_tail_when_asked(tmp_path) -> None:
458
+ """The 17n+5 grid only snaps down, so part of every clip is dropped. A user whose action sits in
459
+ the last half second needs the tail rather than the head."""
460
+ from inline_core.training import h3
461
+
462
+ clip = _write_clip(tmp_path / "ramp.mp4", frames=40)
463
+ head = h3._clip_frames(clip, clip_frames=24, window="start")
464
+ tail = h3._clip_frames(clip, clip_frames=24, window="end")
465
+
466
+ assert len(head) == len(tail) == 22
467
+ # The fixture ramps brightness per frame, so the two windows cannot be the same footage.
468
+ assert head[0].getpixel((0, 0)) != tail[0].getpixel((0, 0))
469
+
470
+
471
+ def test_clip_window_defaults_to_the_start(tmp_path) -> None:
472
+ from inline_core.training import h3
473
+
474
+ clip = _write_clip(tmp_path / "ramp2.mp4", frames=40)
475
+ assert h3._clip_frames(clip, clip_frames=24)[0].getpixel((0, 0)) == (
476
+ h3._clip_frames(clip, clip_frames=24, window="start")[0].getpixel((0, 0))
477
+ )
@@ -60,15 +60,27 @@ class Sandbox:
60
60
  self.venv_python.parent.mkdir(parents=True, exist_ok=True)
61
61
  _stub(self.venv_python, 'echo "$*" >> "$PYTHON_LOG"\nexit 0\n')
62
62
 
63
- def pretend_nvidia_gpu(self, *compute_caps: str) -> None:
63
+ def pretend_nvidia_gpu(self, *compute_caps: str, driver: str = "") -> None:
64
64
  """A driver that answers `-L` and the compute_cap query. Pass no caps for an older driver
65
- that does not know the query, which is the case the fallback index exists for."""
66
- answer = "".join(f"printf '{cap}\\n'\n" for cap in compute_caps) or "exit 1\n"
65
+ that does not know the query, which is the case the fallback index exists for.
66
+
67
+ `driver` fills the second CSV column; the launcher reads it for the R580 floor that decides
68
+ cu130 against frozen cu128."""
69
+ suffix = f", {driver}" if driver else ""
70
+ answer = "".join(f"printf '{cap}{suffix}\\n'\n" for cap in compute_caps) or "exit 1\n"
67
71
  _stub(
68
72
  self.stubs / "nvidia-smi",
69
73
  f'case "$*" in\n *compute_cap*) {answer.strip()} ;;\nesac\nexit 0\n',
70
74
  )
71
75
 
76
+ def pretend_nvidia_probe_errors(self) -> None:
77
+ """A driver that lists GPUs but answers the query with an error string. The word must never
78
+ be coerced into a capability."""
79
+ _stub(
80
+ self.stubs / "nvidia-smi",
81
+ 'case "$*" in\n *compute_cap*) printf \'Unknown Error\\n\' ;;\nesac\nexit 0\n',
82
+ )
83
+
72
84
  def pretend_windows(self) -> None:
73
85
  """Only Windows needs an explicit CUDA index; this script runs there under Git Bash."""
74
86
  _stub(self.stubs / "uname", "printf 'MINGW64_NT-10.0-22631\\n'\nexit 0\n")
@@ -251,3 +263,76 @@ def test_launch_prefers_our_venv_over_the_active_environment(sandbox: Sandbox) -
251
263
  assert "FOREIGN" not in ran
252
264
  assert "-m inline_core.server" in ran
253
265
  assert "not the environment active in this shell" in done.stdout
266
+
267
+
268
+ # --- the detect-only flag, and the decisions it reports -----------------------------------------
269
+ #
270
+ # Two field reports were unfalsifiable because the launcher printed a conclusion and never the
271
+ # evidence. --print-torch-index prints both and installs nothing.
272
+
273
+
274
+ def _decision(sandbox: Sandbox, *args: str) -> dict[str, str]:
275
+ result = sandbox.run("--print-torch-index", *args)
276
+ assert result.returncode == 0, result.stderr
277
+ out = {}
278
+ for line in result.stdout.splitlines():
279
+ key, _, value = line.partition(": ")
280
+ out[key] = value
281
+ return out
282
+
283
+
284
+ def test_print_torch_index_installs_nothing(sandbox: Sandbox) -> None:
285
+ sandbox.pretend_windows()
286
+ sandbox.pretend_nvidia_gpu("12.0", driver="610.88")
287
+ _decision(sandbox)
288
+ assert sandbox.uv_calls() == []
289
+
290
+
291
+ def test_blackwell_on_a_current_driver_gets_cu130(sandbox: Sandbox) -> None:
292
+ sandbox.pretend_windows()
293
+ sandbox.pretend_nvidia_gpu("12.0", driver="610.88")
294
+ got = _decision(sandbox)
295
+ assert got["torch-index"] == "cu130"
296
+ assert got["capability-major"] == "12"
297
+ assert got["reason"] == "autodetect"
298
+
299
+
300
+ def test_blackwell_on_an_old_driver_gets_frozen_cu128(sandbox: Sandbox) -> None:
301
+ """CUDA 13 needs R580. cu128 still serves and was the first index with sm_120, so that machine
302
+ has exactly one workable choice and should not be made to type it back to us."""
303
+ sandbox.pretend_windows()
304
+ sandbox.pretend_nvidia_gpu("12.0", driver="575.57")
305
+ got = _decision(sandbox)
306
+ assert got["torch-index"] == "cu128"
307
+ assert got["reason"] == "driver-floor-cu128"
308
+
309
+
310
+ def test_ada_gets_cu126(sandbox: Sandbox) -> None:
311
+ sandbox.pretend_windows()
312
+ sandbox.pretend_nvidia_gpu("8.9", driver="580.00")
313
+ assert _decision(sandbox)["torch-index"] == "cu126"
314
+
315
+
316
+ def test_a_garbage_probe_falls_back_and_shows_its_working(sandbox: Sandbox) -> None:
317
+ """`Unknown Error` must not become a capability. A string comparison would rank it above 10 and
318
+ hand an unknown card cu130."""
319
+ sandbox.pretend_windows()
320
+ sandbox.pretend_nvidia_probe_errors()
321
+ got = _decision(sandbox)
322
+ assert got["torch-index"] == "cu126"
323
+ assert got["capability-major"] == "unknown"
324
+ assert "Unknown Error" in got["probe"] # the raw line is echoed, not swallowed
325
+
326
+
327
+ def test_no_gpu_reports_why(sandbox: Sandbox) -> None:
328
+ got = _decision(sandbox)
329
+ assert got["torch-index"] == "cpu"
330
+ assert got["reason"] == "no-gpu"
331
+
332
+
333
+ def test_an_explicit_index_is_reported_as_an_override(sandbox: Sandbox) -> None:
334
+ sandbox.pretend_windows()
335
+ sandbox.pretend_nvidia_gpu("12.0", driver="610.88")
336
+ got = _decision(sandbox, "--torch-index", "cu126")
337
+ assert got["torch-index"] == "cu126"
338
+ assert got["reason"] == "override"