horde-engine 6.3.2__tar.gz → 6.3.4__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 (276) hide show
  1. {horde_engine-6.3.2 → horde_engine-6.3.4}/PKG-INFO +1 -1
  2. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/_version.py +2 -2
  3. horde_engine-6.3.4/hordelib/exceptions.py +19 -0
  4. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/model_manager/civitai_adhoc.py +289 -69
  5. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/model_manager/lora.py +190 -61
  6. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/model_manager/ti.py +108 -15
  7. horde_engine-6.3.2/hordelib/exceptions.py +0 -10
  8. {horde_engine-6.3.2 → horde_engine-6.3.4}/.gitignore +0 -0
  9. {horde_engine-6.3.2 → horde_engine-6.3.4}/LICENSE +0 -0
  10. {horde_engine-6.3.2 → horde_engine-6.3.4}/README.md +0 -0
  11. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/__init__.py +0 -0
  12. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/__pyinstaller.py +0 -0
  13. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/api.py +0 -0
  14. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/benchmark.py +0 -0
  15. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/beta_models.py +0 -0
  16. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/comfy_horde.py +0 -0
  17. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/config_path.py +0 -0
  18. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/consts.py +0 -0
  19. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/execution/__init__.py +0 -0
  20. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/execution/comfy_events.py +0 -0
  21. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/execution/comfy_patches.py +0 -0
  22. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/execution/component_materialize.py +0 -0
  23. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/execution/component_stability_probe.py +0 -0
  24. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/execution/graph_utils.py +0 -0
  25. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/execution/in_process.py +0 -0
  26. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/execution/interface.py +0 -0
  27. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/execution/model_dirs.py +0 -0
  28. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/execution/phase_timing.py +0 -0
  29. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/execution/progress_hook.py +0 -0
  30. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/execution/results.py +0 -0
  31. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/execution/sampling_lease.py +0 -0
  32. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/execution/server_shim.py +0 -0
  33. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/execution/shared_components.py +0 -0
  34. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/execution/stage_graph.py +0 -0
  35. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/execution/stage_payloads.py +0 -0
  36. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/execution/zero_copy_load.py +0 -0
  37. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/feature_impact.py +0 -0
  38. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/feature_requirements.py +0 -0
  39. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/horde.py +0 -0
  40. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/initialisation.py +0 -0
  41. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/installation/__init__.py +0 -0
  42. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/installation/__main__.py +0 -0
  43. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/installation/installer.py +0 -0
  44. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/installation/manifest.json +0 -0
  45. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/installation/manifest.py +0 -0
  46. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/integrations/__init__.py +0 -0
  47. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/integrations/logfire_comfy_internals.py +0 -0
  48. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/integrations/logfire_setup.py +0 -0
  49. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/metrics.py +0 -0
  50. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/model_database/db_dep.json +0 -0
  51. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/model_database/db_embeds.json +0 -0
  52. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/model_database/diffusers.json +0 -0
  53. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/model_database/med_config.json +0 -0
  54. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/model_manager/__init__.py +0 -0
  55. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/model_manager/annotator_provider.py +0 -0
  56. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/model_manager/base.py +0 -0
  57. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/model_manager/civitai_provider.py +0 -0
  58. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/model_manager/civitai_records.py +0 -0
  59. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/model_manager/codeformer.py +0 -0
  60. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/model_manager/compvis.py +0 -0
  61. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/model_manager/controlnet.py +0 -0
  62. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/model_manager/controlnet_annotator.py +0 -0
  63. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/model_manager/diffusers.py +0 -0
  64. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/model_manager/esrgan.py +0 -0
  65. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/model_manager/gfpgan.py +0 -0
  66. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/model_manager/hyper.py +0 -0
  67. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/model_manager/miscellaneous.py +0 -0
  68. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/model_manager/safety_checker.py +0 -0
  69. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/__init__.py +0 -0
  70. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/comfyui_layerdiffuse/LICENSE +0 -0
  71. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/comfyui_layerdiffuse/README.md +0 -0
  72. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/comfyui_layerdiffuse/__init__.py +0 -0
  73. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/comfyui_layerdiffuse/examples/layer_diffusion_cond_example.json +0 -0
  74. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/comfyui_layerdiffuse/examples/layer_diffusion_cond_fg_all.json +0 -0
  75. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/comfyui_layerdiffuse/examples/layer_diffusion_cond_joint_bg.json +0 -0
  76. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/comfyui_layerdiffuse/examples/layer_diffusion_cond_joint_fg.json +0 -0
  77. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/comfyui_layerdiffuse/examples/layer_diffusion_diff_bg.json +0 -0
  78. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/comfyui_layerdiffuse/examples/layer_diffusion_diff_bg_stop_at.json +0 -0
  79. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/comfyui_layerdiffuse/examples/layer_diffusion_diff_fg.json +0 -0
  80. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/comfyui_layerdiffuse/examples/layer_diffusion_fg_example.json +0 -0
  81. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/comfyui_layerdiffuse/examples/layer_diffusion_fg_example_rgba.json +0 -0
  82. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/comfyui_layerdiffuse/examples/layer_diffusion_joint.json +0 -0
  83. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/comfyui_layerdiffuse/layered_diffusion.py +0 -0
  84. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/comfyui_layerdiffuse/lib_layerdiffusion/__init__.py +0 -0
  85. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/comfyui_layerdiffuse/lib_layerdiffusion/attention_sharing.py +0 -0
  86. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/comfyui_layerdiffuse/lib_layerdiffusion/enums.py +0 -0
  87. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/comfyui_layerdiffuse/lib_layerdiffusion/models.py +0 -0
  88. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/comfyui_layerdiffuse/lib_layerdiffusion/utils.py +0 -0
  89. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/comfyui_layerdiffuse/pyproject.toml +0 -0
  90. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/comfyui_layerdiffuse/requirements.txt +0 -0
  91. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/LICENSE +0 -0
  92. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/README.md +0 -0
  93. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/__init__.py +0 -0
  94. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/basicsr/VERSION +0 -0
  95. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/basicsr/__init__.py +0 -0
  96. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/basicsr/archs/__init__.py +0 -0
  97. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/basicsr/archs/arcface_arch.py +0 -0
  98. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/basicsr/archs/arch_util.py +0 -0
  99. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/basicsr/archs/codeformer_arch.py +0 -0
  100. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/basicsr/archs/rrdbnet_arch.py +0 -0
  101. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/basicsr/archs/vgg_arch.py +0 -0
  102. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/basicsr/archs/vqgan_arch.py +0 -0
  103. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/basicsr/data/__init__.py +0 -0
  104. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/basicsr/data/data_sampler.py +0 -0
  105. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/basicsr/data/data_util.py +0 -0
  106. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/basicsr/data/prefetch_dataloader.py +0 -0
  107. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/basicsr/data/transforms.py +0 -0
  108. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/basicsr/losses/__init__.py +0 -0
  109. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/basicsr/losses/loss_util.py +0 -0
  110. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/basicsr/losses/losses.py +0 -0
  111. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/basicsr/metrics/__init__.py +0 -0
  112. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/basicsr/metrics/metric_util.py +0 -0
  113. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/basicsr/metrics/psnr_ssim.py +0 -0
  114. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/basicsr/models/__init__.py +0 -0
  115. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/basicsr/ops/__init__.py +0 -0
  116. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/basicsr/ops/dcn/__init__.py +0 -0
  117. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/basicsr/ops/dcn/deform_conv.py +0 -0
  118. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/basicsr/ops/dcn/src/deform_conv_cuda.cpp +0 -0
  119. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/basicsr/ops/dcn/src/deform_conv_cuda_kernel.cu +0 -0
  120. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/basicsr/ops/dcn/src/deform_conv_ext.cpp +0 -0
  121. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/basicsr/ops/fused_act/__init__.py +0 -0
  122. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/basicsr/ops/fused_act/fused_act.py +0 -0
  123. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/basicsr/ops/fused_act/src/fused_bias_act.cpp +0 -0
  124. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/basicsr/ops/fused_act/src/fused_bias_act_kernel.cu +0 -0
  125. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/basicsr/ops/upfirdn2d/__init__.py +0 -0
  126. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/basicsr/ops/upfirdn2d/src/upfirdn2d.cpp +0 -0
  127. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/basicsr/ops/upfirdn2d/src/upfirdn2d_kernel.cu +0 -0
  128. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/basicsr/ops/upfirdn2d/upfirdn2d.py +0 -0
  129. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/basicsr/setup.py +0 -0
  130. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/basicsr/train.py +0 -0
  131. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/basicsr/utils/__init__.py +0 -0
  132. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/basicsr/utils/dist_util.py +0 -0
  133. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/basicsr/utils/download_util.py +0 -0
  134. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/basicsr/utils/file_client.py +0 -0
  135. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/basicsr/utils/img_util.py +0 -0
  136. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/basicsr/utils/lmdb_util.py +0 -0
  137. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/basicsr/utils/logger.py +0 -0
  138. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/basicsr/utils/matlab_functions.py +0 -0
  139. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/basicsr/utils/misc.py +0 -0
  140. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/basicsr/utils/options.py +0 -0
  141. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/basicsr/utils/realesrgan_utils.py +0 -0
  142. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/basicsr/utils/registry.py +0 -0
  143. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/basicsr/version.py +0 -0
  144. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/facelib/__init__.py +0 -0
  145. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/facelib/detection/__init__.py +0 -0
  146. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/facelib/detection/align_trans.py +0 -0
  147. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/facelib/detection/matlab_cp2tform.py +0 -0
  148. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/facelib/detection/retinaface/retinaface.py +0 -0
  149. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/facelib/detection/retinaface/retinaface_net.py +0 -0
  150. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/facelib/detection/retinaface/retinaface_utils.py +0 -0
  151. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/facelib/detection/yolov5face/__init__.py +0 -0
  152. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/facelib/detection/yolov5face/face_detector.py +0 -0
  153. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/facelib/detection/yolov5face/models/__init__.py +0 -0
  154. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/facelib/detection/yolov5face/models/common.py +0 -0
  155. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/facelib/detection/yolov5face/models/experimental.py +0 -0
  156. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/facelib/detection/yolov5face/models/yolo.py +0 -0
  157. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/facelib/detection/yolov5face/models/yolov5l.yaml +0 -0
  158. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/facelib/detection/yolov5face/models/yolov5n.yaml +0 -0
  159. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/facelib/detection/yolov5face/utils/__init__.py +0 -0
  160. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/facelib/detection/yolov5face/utils/autoanchor.py +0 -0
  161. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/facelib/detection/yolov5face/utils/datasets.py +0 -0
  162. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/facelib/detection/yolov5face/utils/extract_ckpt.py +0 -0
  163. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/facelib/detection/yolov5face/utils/general.py +0 -0
  164. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/facelib/detection/yolov5face/utils/torch_utils.py +0 -0
  165. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/facelib/parsing/__init__.py +0 -0
  166. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/facelib/parsing/bisenet.py +0 -0
  167. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/facelib/parsing/parsenet.py +0 -0
  168. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/facelib/parsing/resnet.py +0 -0
  169. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/facelib/utils/__init__.py +0 -0
  170. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/facelib/utils/face_restoration_helper.py +0 -0
  171. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/facelib/utils/face_utils.py +0 -0
  172. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/facelib/utils/misc.py +0 -0
  173. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/r_chainner/README.md +0 -0
  174. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/r_chainner/__init__.py +0 -0
  175. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/r_chainner/gfpganv1_clean_arch.py +0 -0
  176. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/r_chainner/model_loading.py +0 -0
  177. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/r_chainner/stylegan2_clean_arch.py +0 -0
  178. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/facerestore_cf/r_chainner/types.py +0 -0
  179. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/node_controlnet_model_loader.py +0 -0
  180. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/node_image_loader.py +0 -0
  181. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/node_image_output.py +0 -0
  182. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/node_lora_loader.py +0 -0
  183. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/node_model_loader.py +0 -0
  184. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/node_stage_io.py +0 -0
  185. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/node_upscale_model_loader.py +0 -0
  186. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/nodes/node_v3_canary.py +0 -0
  187. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipeline/__init__.py +0 -0
  188. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipeline/comfy_node_inputs.json +0 -0
  189. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipeline/constants.py +0 -0
  190. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipeline/context.py +0 -0
  191. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipeline/definition.py +0 -0
  192. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipeline/families/__init__.py +0 -0
  193. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipeline/families/image.py +0 -0
  194. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipeline/families/image_gen/__init__.py +0 -0
  195. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipeline/families/image_gen/baselines.py +0 -0
  196. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipeline/families/image_gen/bindings.py +0 -0
  197. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipeline/families/image_gen/cascade.py +0 -0
  198. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipeline/families/image_gen/controlnet.py +0 -0
  199. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipeline/families/image_gen/creative_upscale.py +0 -0
  200. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipeline/families/image_gen/features.py +0 -0
  201. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipeline/families/image_gen/flux.py +0 -0
  202. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipeline/families/image_gen/qr_code.py +0 -0
  203. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipeline/families/image_gen/qwen.py +0 -0
  204. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipeline/families/image_gen/stable_diffusion.py +0 -0
  205. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipeline/families/image_gen/steps.py +0 -0
  206. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipeline/families/image_gen/z_image.py +0 -0
  207. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipeline/families/post_processing.py +0 -0
  208. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipeline/graph.py +0 -0
  209. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipeline/horde_compat.py +0 -0
  210. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipeline/identifiers.py +0 -0
  211. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipeline/node_schemas.py +0 -0
  212. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipeline/patches.py +0 -0
  213. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipeline/payload.py +0 -0
  214. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipeline/payload_pp.py +0 -0
  215. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipeline/registry.py +0 -0
  216. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipeline/resolution.py +0 -0
  217. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipeline/sdk_adapter.py +0 -0
  218. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipeline_designs/pipeline_controlnet.json +0 -0
  219. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipeline_designs/pipeline_controlnet_annotator.json +0 -0
  220. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipeline_designs/pipeline_controlnet_hires_fix.json +0 -0
  221. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipeline_designs/pipeline_flux.json +0 -0
  222. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipeline_designs/pipeline_image_facefix.json +0 -0
  223. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipeline_designs/pipeline_image_upscale.json +0 -0
  224. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipeline_designs/pipeline_qr_code.json +0 -0
  225. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipeline_designs/pipeline_qwen.json +0 -0
  226. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipeline_designs/pipeline_stable_cascade.json +0 -0
  227. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipeline_designs/pipeline_stable_cascade_2pass.json +0 -0
  228. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipeline_designs/pipeline_stable_cascade_remix.json +0 -0
  229. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipeline_designs/pipeline_stable_diffusion.json +0 -0
  230. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipeline_designs/pipeline_stable_diffusion_hires_fix.json +0 -0
  231. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipeline_designs/pipeline_stable_diffusion_img2img_mask.json +0 -0
  232. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipeline_designs/pipeline_stable_diffusion_paint.json +0 -0
  233. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipelines/pipeline_controlnet.json +0 -0
  234. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipelines/pipeline_controlnet_annotator.json +0 -0
  235. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipelines/pipeline_controlnet_hires_fix.json +0 -0
  236. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipelines/pipeline_creative_upscale.json +0 -0
  237. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipelines/pipeline_flux.json +0 -0
  238. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipelines/pipeline_image_facefix.json +0 -0
  239. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipelines/pipeline_image_upscale.json +0 -0
  240. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipelines/pipeline_qr_code.json +0 -0
  241. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipelines/pipeline_qwen.json +0 -0
  242. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipelines/pipeline_stable_cascade.json +0 -0
  243. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipelines/pipeline_stable_cascade_2pass.json +0 -0
  244. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipelines/pipeline_stable_cascade_remix.json +0 -0
  245. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipelines/pipeline_stable_diffusion.json +0 -0
  246. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipelines/pipeline_stable_diffusion_hires_fix.json +0 -0
  247. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipelines/pipeline_stable_diffusion_img2img_mask.json +0 -0
  248. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipelines/pipeline_stable_diffusion_paint.json +0 -0
  249. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pipelines/pipeline_z_image.json +0 -0
  250. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/preload.py +0 -0
  251. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/profiling.py +0 -0
  252. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/py.typed +0 -0
  253. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pyinstaller_hooks/__init__.py +0 -0
  254. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/pyinstaller_hooks/hook-hordelib.horde.py +0 -0
  255. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/safety.py +0 -0
  256. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/safety_checker.py +0 -0
  257. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/settings.py +0 -0
  258. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/shared_model_manager.py +0 -0
  259. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/train.py +0 -0
  260. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/utils/__init__.py +0 -0
  261. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/utils/device_pinning.py +0 -0
  262. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/utils/distance.py +0 -0
  263. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/utils/dynamicprompt.py +0 -0
  264. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/utils/fd_limits.py +0 -0
  265. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/utils/gpuinfo.py +0 -0
  266. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/utils/image_utils.py +0 -0
  267. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/utils/ioredirect.py +0 -0
  268. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/utils/logger.py +0 -0
  269. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/utils/nvml.py +0 -0
  270. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/utils/optional_deps.py +0 -0
  271. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/utils/sanitizer.py +0 -0
  272. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/utils/switch.py +0 -0
  273. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/utils/torch_build.py +0 -0
  274. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/utils/torch_memory.py +0 -0
  275. {horde_engine-6.3.2 → horde_engine-6.3.4}/hordelib/vram_planning.py +0 -0
  276. {horde_engine-6.3.2 → horde_engine-6.3.4}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: horde-engine
3
- Version: 6.3.2
3
+ Version: 6.3.4
4
4
  Summary: A wrapper around ComfyUI to allow use by AI Horde.
5
5
  Project-URL: Homepage, https://github.com/Haidra-Org/hordelib
6
6
  Project-URL: Bug Tracker, https://github.com/Haidra-Org/hordelib/issues
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '6.3.2'
22
- __version_tuple__ = version_tuple = (6, 3, 2)
21
+ __version__ = version = '6.3.4'
22
+ __version_tuple__ = version_tuple = (6, 3, 4)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -0,0 +1,19 @@
1
+ class CivitAIDown(Exception):
2
+ pass
3
+
4
+
5
+ class CivitAIResourceNotFound(Exception):
6
+ """A CivitAI metadata request definitively failed with a client error (a not-found or unauthorized status).
7
+
8
+ Distinct from :class:`CivitAIDown`: that signals a transient outage worth retrying, whereas this signals a
9
+ terminal verdict (the reference does not exist, or is not accessible) that retrying the same request cannot
10
+ change. Callers translate it into a terminal rejection rather than a retryable failure.
11
+ """
12
+
13
+
14
+ class ModelInvalid(Exception):
15
+ pass
16
+
17
+
18
+ class ModelEmpty(Exception):
19
+ pass
@@ -30,7 +30,7 @@ import time
30
30
  import uuid
31
31
  from abc import abstractmethod
32
32
  from collections import deque
33
- from collections.abc import Iterable
33
+ from collections.abc import Callable, Iterable
34
34
  from contextlib import AbstractContextManager, nullcontext
35
35
  from dataclasses import dataclass, field
36
36
  from datetime import datetime
@@ -45,9 +45,16 @@ from horde_model_reference.meta_consts import MODEL_REFERENCE_CATEGORY
45
45
  from horde_model_reference.model_reference_records import GenericModelRecord
46
46
  from loguru import logger
47
47
 
48
+ from hordelib.exceptions import CivitAIResourceNotFound
48
49
  from hordelib.metrics import DownloadCategory, DownloadEvent, get_metrics_collector
49
50
  from hordelib.model_manager.base import BaseModelManager
50
51
 
52
+ try:
53
+ from horde_model_reference.download_engine import DownloadOutcome, _segmented_download
54
+ except ImportError: # pragma: no cover - the engine ships with horde_model_reference; degrade to single-stream
55
+ _segmented_download = None # type: ignore[assignment]
56
+ DownloadOutcome = None # type: ignore[assignment,misc]
57
+
51
58
  TESTS_ONGOING = os.getenv("TESTS_ONGOING", "0") == "1"
52
59
 
53
60
  TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S"
@@ -86,6 +93,24 @@ fails (its own cleanup) well inside this window, so only a crashed writer's temp
86
93
  Age-gating the prune reclaims leaked disk without ever deleting an in-flight write.
87
94
  """
88
95
 
96
+ DOWNLOAD_CHUNK_SIZE = 1024 * 1024
97
+ """Bytes pulled from the network per streamed read of a weight download.
98
+
99
+ A weight file is transferred a chunk at a time rather than buffered whole in process memory (one LoRA
100
+ can be hundreds of MB), so only one chunk is resident at once. The chunk boundary is also where an
101
+ in-flight transfer observes a cancellation and reports progress, so it doubles as the responsiveness
102
+ granularity: large enough to keep per-chunk overhead negligible, small enough that a cancelled or
103
+ throttled stream reacts promptly.
104
+ """
105
+
106
+ DEFAULT_ADHOC_DOWNLOAD_CONNECTIONS = 4
107
+ """Concurrent ranged connections for a single large ad-hoc LoRA, matching the worker's checkpoint default.
108
+
109
+ A single TCP stream to Cloudflare-fronted CivitAI is capped near the per-connection rate limit, so a large
110
+ LoRA is fetched over several ranged connections to raise throughput. Small LoRAs and range-refusing servers
111
+ fall through to the single-stream path unchanged. Overridable via ``AIWORKER_LORA_DOWNLOAD_CONNECTIONS``."""
112
+
113
+
89
114
  DEFAULT_MIN_FREE_DISK_MB = 1024
90
115
  """Keep at least this many MB free on the cache volume; ad-hoc downloads evict (then refuse) below it.
91
116
 
@@ -110,6 +135,17 @@ def timestamp_to_datetime(value: str | None) -> datetime | None:
110
135
  return None
111
136
 
112
137
 
138
+ def _adhoc_connections_from_env() -> int:
139
+ """The configured concurrent-connection count for large ad-hoc LoRA downloads (>= 1)."""
140
+ raw = os.getenv("AIWORKER_LORA_DOWNLOAD_CONNECTIONS")
141
+ if raw is None:
142
+ return DEFAULT_ADHOC_DOWNLOAD_CONNECTIONS
143
+ try:
144
+ return max(1, int(raw))
145
+ except ValueError:
146
+ return DEFAULT_ADHOC_DOWNLOAD_CONNECTIONS
147
+
148
+
113
149
  def normalise_a1111_triggers(triggers: list[str]) -> list[str]:
114
150
  """Return *triggers* with any A1111-style ``<lora:name:weight>`` entries reduced to ``name``."""
115
151
  return [
@@ -163,6 +199,16 @@ class SkipDownload(Exception):
163
199
  """
164
200
 
165
201
 
202
+ class _DownloadCancelled(Exception):
203
+ """Internal signal that an in-flight streamed transfer was abandoned because its job was cancelled.
204
+
205
+ Raised from the streaming loop when the manager's cancellation generation moves past the value the
206
+ download was enqueued under, so a caller that has given up frees the shared worker without the
207
+ transfer running to completion. Handled entirely within :meth:`CivitaiAdhocModelManager._run_download`;
208
+ never surfaces to callers.
209
+ """
210
+
211
+
166
212
  class ReadOnlyModelManagerError(RuntimeError):
167
213
  """Raised when a write, download, or eviction is attempted on a read-only manager.
168
214
 
@@ -184,6 +230,22 @@ class _QueuedDownload[R]:
184
230
  A worker thread abandons an in-flight or queued download once the manager's current generation has
185
231
  moved past the value stamped here (see :meth:`CivitaiAdhocModelManager.cancel_active_downloads`),
186
232
  so a caller that has given up on a job can free the shared pool without killing its threads."""
233
+ progress_callback: Callable[[int, int], None] | None = None
234
+ """Optional per-chunk progress hook invoked as ``(downloaded_bytes, total_bytes)``.
235
+
236
+ ``total_bytes`` is the ``Content-Length`` of the transfer, or ``0`` when the server does not report
237
+ one. Invoked once per streamed chunk while the weight file is being written."""
238
+ completion_event: threading.Event = field(default_factory=threading.Event)
239
+ """Set exactly once when this download reaches any terminal outcome.
240
+
241
+ Lets a caller wait on this one record finishing rather than on the whole pool going idle, so
242
+ unrelated queue traffic (the default-model seed, another job's ad-hoc fetch) cannot blow a bounded
243
+ per-record wait. Read :attr:`success` after the event is set to learn the outcome."""
244
+ success: bool | None = None
245
+ """The terminal outcome, readable once :attr:`completion_event` is set.
246
+
247
+ ``True`` when the weight file is present and committed (freshly downloaded or already on disk and
248
+ verified), ``False`` on any failure/skip/cancellation, and ``None`` while still in flight."""
187
249
 
188
250
 
189
251
  class CivitaiAdhocModelManager[RecordT: GenericModelRecord](
@@ -298,6 +360,7 @@ class CivitaiAdhocModelManager[RecordT: GenericModelRecord](
298
360
  # freeing the (shared, daemon) pool for the next job without tearing the threads down. Guarded by
299
361
  # _download_mutex for writes; read locklessly in the worker loop (a plain int read is atomic).
300
362
  self._download_generation = 0
363
+ self._download_connections = _adhoc_connections_from_env()
301
364
  self._index_ids: dict[int, str] = {}
302
365
  self._index_orig_names: dict[str, str] = {}
303
366
  self.total_retries_attempted = 0
@@ -583,9 +646,15 @@ class CivitaiAdhocModelManager[RecordT: GenericModelRecord](
583
646
  def _fetch_civitai_json(self, url: str) -> dict | None:
584
647
  """Return the parsed JSON body of a CivitAI API GET, retrying transient failures.
585
648
 
586
- Returns ``None`` on a definitive failure (client error, exhausted retries, or repeated
587
- invalid JSON), mirroring CivitAI's quirk of returning 500 for impossible ids and HTML for
588
- some error pages.
649
+ Returns ``None`` on a transient give-up (an exhausted retry budget, a repeated 5xx, or repeated
650
+ invalid JSON), mirroring CivitAI's quirk of returning 500 for impossible ids and HTML for some
651
+ error pages; a caller cannot tell that outcome apart from a slow-then-recovering outage, so it stays
652
+ retryable.
653
+
654
+ Raises:
655
+ CivitAIResourceNotFound: On a definitive client error (a 401 or 404). Unlike the transient
656
+ ``None`` this is terminal: retrying the same public request will not change it, so the caller
657
+ translates it into a terminal rejection rather than a retryable failure.
589
658
  """
590
659
  fetch_logger = logger.bind(manager=self.METRIC_PREFIX)
591
660
  start_time = time.perf_counter()
@@ -618,8 +687,15 @@ class CivitaiAdhocModelManager[RecordT: GenericModelRecord](
618
687
  status_code=status_code,
619
688
  )
620
689
 
621
- # 401/404 and 5xx above 500 will not resolve by retrying; give up immediately.
622
- if status_code in (401, 404) or (status_code is not None and status_code > 500):
690
+ # A 401/404 is a definitive verdict, not a transient failure: the reference does not exist or
691
+ # is inaccessible, so surface it distinctly rather than as an indistinguishable retryable None.
692
+ if status_code in (401, 404):
693
+ raise CivitAIResourceNotFound(
694
+ f"CivitAI returned {status_code} for {truncated_url}",
695
+ ) from fetch_error
696
+ # A 5xx above 500 will not resolve by retrying either, but it is a server-side fault (not a
697
+ # verdict on the reference), so it stays a transient give-up the caller may retry later.
698
+ if status_code is not None and status_code > 500:
623
699
  return None
624
700
  # CivitAI's 500s and HTML-instead-of-JSON pages rarely clear quickly; burn most retries.
625
701
  if status_code == 500 or isinstance(fetch_error, json.JSONDecodeError):
@@ -644,9 +720,19 @@ class CivitaiAdhocModelManager[RecordT: GenericModelRecord](
644
720
  # Download queue and worker threads
645
721
  # ------------------------------------------------------------------
646
722
 
647
- def _enqueue_download(self, record: RecordT, context: dict | None = None) -> None:
723
+ def _enqueue_download(
724
+ self,
725
+ record: RecordT,
726
+ context: dict | None = None,
727
+ *,
728
+ progress_callback: Callable[[int, int], None] | None = None,
729
+ ) -> _QueuedDownload[RecordT]:
648
730
  """Ensure the worker pool is running and append *record* to the download queue.
649
731
 
732
+ Returns:
733
+ The enqueued :class:`_QueuedDownload`, whose ``completion_event`` a caller can wait on to
734
+ block until this specific record finishes (rather than the whole pool going idle).
735
+
650
736
  Raises:
651
737
  ReadOnlyModelManagerError: If the manager is read-only.
652
738
  """
@@ -660,10 +746,15 @@ class CivitaiAdhocModelManager[RecordT: GenericModelRecord](
660
746
  worker.start()
661
747
  self._metric_active_threads.set(len(self._download_threads))
662
748
 
663
- self._download_queue.append(
664
- _QueuedDownload(record=record, context=context or {}, generation=self._download_generation),
749
+ queued = _QueuedDownload(
750
+ record=record,
751
+ context=context or {},
752
+ generation=self._download_generation,
753
+ progress_callback=progress_callback,
665
754
  )
755
+ self._download_queue.append(queued)
666
756
  self._metric_queue_size.set(len(self._download_queue))
757
+ return queued
667
758
 
668
759
  def _download_thread(self, thread_number: int) -> None:
669
760
  """Worker loop: dequeue records and download their weight files until stopped.
@@ -743,7 +834,31 @@ class CivitaiAdhocModelManager[RecordT: GenericModelRecord](
743
834
  return True
744
835
 
745
836
  def _process_download(self, queued: _QueuedDownload, thread_logger) -> None:
746
- """Download a single queued record's weight file, retrying transient failures."""
837
+ """Download a queued record, then signal its completion event exactly once.
838
+
839
+ Delegates the transfer to :meth:`_run_download` and, whatever the outcome, records the result on
840
+ the queued item (:attr:`_QueuedDownload.success`) and sets its completion event so a caller
841
+ waiting on this one record is released regardless of which exit path was taken.
842
+ """
843
+ try:
844
+ queued.success = self._run_download(queued, thread_logger)
845
+ finally:
846
+ queued.completion_event.set()
847
+
848
+ def _content_length(self, response: requests.Response) -> int:
849
+ """Return the response's ``Content-Length`` in bytes, or ``0`` when it is absent or malformed."""
850
+ try:
851
+ return int(response.headers.get("Content-Length") or 0)
852
+ except (TypeError, ValueError):
853
+ return 0
854
+
855
+ def _run_download(self, queued: _QueuedDownload, thread_logger) -> bool:
856
+ """Download a single queued record's weight file, retrying transient failures.
857
+
858
+ Returns:
859
+ ``True`` when the weight file is present and committed (freshly downloaded, or already on
860
+ disk and verified); ``False`` on any skip, cancellation, or terminal/exhausted failure.
861
+ """
747
862
  record = queued.record
748
863
  download_logger = thread_logger.bind(model_name=record.name)
749
864
 
@@ -752,10 +867,10 @@ class CivitaiAdhocModelManager[RecordT: GenericModelRecord](
752
867
  notfound_attempts = 0
753
868
  cache_key = record.name # Refined to the version id once the target is prepared.
754
869
  while retries <= self.MAX_RETRIES:
755
- # Abandon a download whose job has been cancelled (the caller gave up and freed the slot): a
756
- # single-shot requests.get cannot be interrupted mid-transfer, but checking here bails before
757
- # the next attempt or retry sleep, so a wedged retry ladder stops within ~one request timeout
758
- # and the shared pool is free for the next job instead of pinning a thread on dead work.
870
+ # Abandon a download whose job has been cancelled (the caller gave up and freed the slot):
871
+ # checking here bails before the next attempt or retry sleep, so a wedged retry ladder stops
872
+ # within ~one request timeout and the shared pool is free for the next job instead of pinning
873
+ # a thread on dead work. An in-flight stream also honours this at each chunk boundary below.
759
874
  if queued.generation != self._download_generation:
760
875
  download_logger.info("adhoc.download_cancelled", retries=retries)
761
876
  self._record_download_event(
@@ -765,11 +880,11 @@ class CivitaiAdhocModelManager[RecordT: GenericModelRecord](
765
880
  duration_seconds=time.perf_counter() - overall_start,
766
881
  retries=retries,
767
882
  )
768
- return
883
+ return False
769
884
  try:
770
885
  target = self._prepare_download(record)
771
886
  if target is None:
772
- return
887
+ return False
773
888
 
774
889
  cache_key = self._download_cache_key(record, target)
775
890
  if self._is_version_bad(cache_key):
@@ -781,7 +896,7 @@ class CivitaiAdhocModelManager[RecordT: GenericModelRecord](
781
896
  duration_seconds=time.perf_counter() - overall_start,
782
897
  retries=retries,
783
898
  )
784
- return
899
+ return False
785
900
  filepath = os.path.join(self.model_folder_path, target.filename)
786
901
  hashpath = f"{os.path.splitext(filepath)[0]}.sha256"
787
902
 
@@ -790,7 +905,7 @@ class CivitaiAdhocModelManager[RecordT: GenericModelRecord](
790
905
  if self.is_default_cache_full():
791
906
  self.stop_downloading = True
792
907
  self.save_reference_to_disk()
793
- return
908
+ return True
794
909
 
795
910
  # Refuse to fetch a fresh weight when the volume can't hold it above the floor, even
796
911
  # after evicting every ad-hoc entry. Writing anyway risks an ENOSPC that takes the
@@ -812,72 +927,134 @@ class CivitaiAdhocModelManager[RecordT: GenericModelRecord](
812
927
  duration_seconds=time.perf_counter() - overall_start,
813
928
  retries=retries,
814
929
  )
815
- return
930
+ return False
816
931
 
817
932
  download_start = time.perf_counter()
818
933
  download_url = target.url
819
934
  if self._civitai_api_token and self.is_model_url_from_civitai(download_url):
820
935
  download_url += f"{'&' if '?' in download_url else '?'}token={self._civitai_api_token}"
821
936
 
822
- response = requests.get(download_url, timeout=self.REQUEST_DOWNLOAD_TIMEOUT)
823
- response.raise_for_status()
824
- if "reason=download-auth" in response.url:
825
- download_logger.error("adhoc.download_auth_redirect", response_url=response.url)
826
- self._mark_version_bad(cache_key)
827
- self._record_download_event(
828
- record,
829
- success=False,
830
- size_bytes=0,
831
- duration_seconds=time.perf_counter() - overall_start,
832
- retries=retries,
833
- )
834
- return
835
-
836
- sha256 = hashlib.sha256(response.content).hexdigest()
837
- if target.sha256 and sha256.lower() != target.sha256.lower():
838
- download_logger.warning(
839
- "adhoc.download_hash_mismatch",
840
- expected=target.sha256[:16],
841
- actual=sha256[:16],
842
- attempt=retries + 1,
843
- )
844
- retries += 1
845
- self.total_retries_attempted += 1
846
- if retries > self.MAX_RETRIES:
847
- self._record_download_event(
848
- record,
849
- success=False,
850
- size_bytes=0,
851
- duration_seconds=time.perf_counter() - overall_start,
852
- retries=retries,
853
- )
854
- return
855
- time.sleep(self.RETRY_DELAY)
856
- continue
857
-
858
- # Stage the weight bytes to a temp file and rename into place so a concurrent reader
859
- # (another process loading this model) never observes a torn or truncated file at the
860
- # final path. The sidecar lands only after the weight file is fully placed, so a present
861
- # sidecar always describes a complete weight file; a weight file without a sidecar is
862
- # harmlessly re-verified or re-downloaded.
863
- self._atomic_write_bytes(filepath, response.content)
864
- self._atomic_write_bytes(hashpath, f"{sha256} *{target.filename}".encode())
937
+ segmented = self._try_segmented_download(download_url, filepath, target, queued)
938
+ if segmented is not None:
939
+ if not segmented.success:
940
+ # The segmented transfer landed a file whose sha256 does not match the record: discard it
941
+ # and retry (or, on the final attempt, fail) exactly as the single-stream hash-mismatch
942
+ # path does.
943
+ self._discard_partial(filepath, hashpath)
944
+ retries += 1
945
+ self.total_retries_attempted += 1
946
+ if retries > self.MAX_RETRIES:
947
+ self._record_download_event(
948
+ record,
949
+ success=False,
950
+ size_bytes=0,
951
+ duration_seconds=time.perf_counter() - overall_start,
952
+ retries=retries,
953
+ )
954
+ return False
955
+ time.sleep(self.RETRY_DELAY)
956
+ continue
957
+ downloaded_bytes = segmented.bytes_written
958
+ else:
959
+ # Stream the weight a chunk at a time into a sibling temp file, hashing as we go, rather
960
+ # than buffering the whole (often hundreds-of-MB) file in memory. The read timeout applies
961
+ # per chunk when streaming, so it bounds a stalled stream without capping a legitimately
962
+ # long transfer, and the temp file is renamed into place only after the checksum passes so
963
+ # a concurrent reader never observes a torn or partially written weight at the final path.
964
+ with requests.get(download_url, stream=True, timeout=self.REQUEST_DOWNLOAD_TIMEOUT) as response:
965
+ response.raise_for_status()
966
+ if "reason=download-auth" in response.url:
967
+ download_logger.error("adhoc.download_auth_redirect", response_url=response.url)
968
+ self._mark_version_bad(cache_key)
969
+ self._record_download_event(
970
+ record,
971
+ success=False,
972
+ size_bytes=0,
973
+ duration_seconds=time.perf_counter() - overall_start,
974
+ retries=retries,
975
+ )
976
+ return False
977
+
978
+ total_bytes = self._content_length(response)
979
+ hasher = hashlib.sha256()
980
+ downloaded_bytes = 0
981
+ tmp_path = f"{filepath}.tmp-{uuid.uuid4().hex[:8]}"
982
+ try:
983
+ with open(tmp_path, "wb") as tmp_file:
984
+ for chunk in response.iter_content(chunk_size=DOWNLOAD_CHUNK_SIZE):
985
+ # A generation bump means the owning job gave up; stop at this chunk
986
+ # boundary so a cancelled stream frees the shared pool promptly instead
987
+ # of transferring to completion.
988
+ if queued.generation != self._download_generation:
989
+ raise _DownloadCancelled
990
+ if not chunk:
991
+ continue
992
+ tmp_file.write(chunk)
993
+ hasher.update(chunk)
994
+ downloaded_bytes += len(chunk)
995
+ if queued.progress_callback is not None:
996
+ queued.progress_callback(downloaded_bytes, total_bytes)
997
+
998
+ sha256 = hasher.hexdigest()
999
+ if target.sha256 and sha256.lower() != target.sha256.lower():
1000
+ download_logger.warning(
1001
+ "adhoc.download_hash_mismatch",
1002
+ expected=target.sha256[:16],
1003
+ actual=sha256[:16],
1004
+ attempt=retries + 1,
1005
+ )
1006
+ retries += 1
1007
+ self.total_retries_attempted += 1
1008
+ if retries > self.MAX_RETRIES:
1009
+ self._record_download_event(
1010
+ record,
1011
+ success=False,
1012
+ size_bytes=0,
1013
+ duration_seconds=time.perf_counter() - overall_start,
1014
+ retries=retries,
1015
+ )
1016
+ return False
1017
+ time.sleep(self.RETRY_DELAY)
1018
+ continue
1019
+
1020
+ self._replace_file_with_retry(tmp_path, filepath)
1021
+ finally:
1022
+ if os.path.exists(tmp_path):
1023
+ try:
1024
+ os.remove(tmp_path)
1025
+ except OSError:
1026
+ pass
1027
+
1028
+ # The sidecar lands only after the weight file is fully placed, so a present sidecar
1029
+ # always describes a complete weight file; a weight file without a sidecar is
1030
+ # harmlessly re-verified or re-downloaded.
1031
+ self._atomic_write_bytes(hashpath, f"{sha256} *{target.filename}".encode())
865
1032
 
866
1033
  self._commit_download(record, target, downloaded=True)
867
1034
  self._metric_download_duration.record(time.perf_counter() - download_start)
868
1035
  self._record_download_event(
869
1036
  record,
870
1037
  success=True,
871
- size_bytes=len(response.content),
1038
+ size_bytes=downloaded_bytes,
872
1039
  duration_seconds=time.perf_counter() - download_start,
873
1040
  retries=retries,
874
1041
  )
875
1042
  self._evict_adhoc_over_limit()
876
1043
  self.save_reference_to_disk()
877
1044
  download_logger.info("adhoc.download_success", filename=target.filename)
878
- return
1045
+ return True
1046
+ except _DownloadCancelled:
1047
+ download_logger.info("adhoc.download_cancelled", retries=retries)
1048
+ self._record_download_event(
1049
+ record,
1050
+ success=False,
1051
+ size_bytes=0,
1052
+ duration_seconds=time.perf_counter() - overall_start,
1053
+ retries=retries,
1054
+ )
1055
+ return False
879
1056
  except SkipDownload:
880
- return
1057
+ return False
881
1058
  except (requests.HTTPError, requests.ConnectionError, requests.Timeout, json.JSONDecodeError) as net_error:
882
1059
  error_type = type(net_error).__name__
883
1060
  self._metric_network_errors.add(1, {"error_type": error_type})
@@ -893,7 +1070,7 @@ class CivitaiAdhocModelManager[RecordT: GenericModelRecord](
893
1070
  duration_seconds=time.perf_counter() - overall_start,
894
1071
  retries=retries,
895
1072
  )
896
- return
1073
+ return False
897
1074
  if isinstance(net_error, requests.HTTPError) and status_code == 404:
898
1075
  # A 404 on the signed download URL is deterministic: the file (or its
899
1076
  # token-scoped resource) is gone and the identical URL will not heal. Retrying the
@@ -910,7 +1087,7 @@ class CivitaiAdhocModelManager[RecordT: GenericModelRecord](
910
1087
  duration_seconds=time.perf_counter() - overall_start,
911
1088
  retries=retries,
912
1089
  )
913
- return
1090
+ return False
914
1091
  time.sleep(self.NOTFOUND_CONFIRM_DELAY)
915
1092
  continue
916
1093
  except OSError as disk_error:
@@ -928,7 +1105,7 @@ class CivitaiAdhocModelManager[RecordT: GenericModelRecord](
928
1105
  duration_seconds=time.perf_counter() - overall_start,
929
1106
  retries=retries,
930
1107
  )
931
- return
1108
+ return False
932
1109
  # Any other OS error may be transient; fall through to the shared retry path below.
933
1110
  self._metric_network_errors.add(1, {"error_type": "fatal"})
934
1111
  download_logger.error("adhoc.download_fatal_error", error=str(disk_error))
@@ -947,8 +1124,51 @@ class CivitaiAdhocModelManager[RecordT: GenericModelRecord](
947
1124
  duration_seconds=time.perf_counter() - overall_start,
948
1125
  retries=retries,
949
1126
  )
950
- return
1127
+ return False
951
1128
  time.sleep(self.RETRY_DELAY)
1129
+ return False
1130
+
1131
+ def _try_segmented_download(
1132
+ self,
1133
+ download_url: str,
1134
+ filepath: str,
1135
+ target: DownloadTarget,
1136
+ queued: _QueuedDownload,
1137
+ ) -> DownloadOutcome | None:
1138
+ """Attempt a multi-connection ranged download of a large LoRA; return the outcome or None to fall back.
1139
+
1140
+ Returns None (so the caller uses the single-stream path) when segmentation is disabled, the engine
1141
+ helper is unavailable, or the engine reports the file is unsuitable (small, no range support, or a
1142
+ transient segment failure). A generation bump during transfer raises :class:`_DownloadCancelled`
1143
+ through the progress hook, aborting every segment, exactly as the single-stream path cancels at a
1144
+ chunk boundary.
1145
+ """
1146
+ if _segmented_download is None or self._download_connections <= 1:
1147
+ return None
1148
+
1149
+ def _progress(downloaded_bytes: int, total_bytes: int) -> None:
1150
+ if queued.generation != self._download_generation:
1151
+ raise _DownloadCancelled
1152
+ if queued.progress_callback is not None:
1153
+ queued.progress_callback(downloaded_bytes, total_bytes)
1154
+
1155
+ return _segmented_download(
1156
+ download_url,
1157
+ Path(filepath),
1158
+ expected_sha256=target.sha256 or None,
1159
+ progress_callback=_progress,
1160
+ extra_headers=None,
1161
+ connections=self._download_connections,
1162
+ max_retries=self.MAX_RETRIES,
1163
+ )
1164
+
1165
+ def _discard_partial(self, filepath: str, hashpath: str) -> None:
1166
+ """Remove a hash-mismatched weight file and its sidecar so a retry re-downloads cleanly."""
1167
+ for path in (filepath, hashpath):
1168
+ try:
1169
+ os.remove(path)
1170
+ except OSError:
1171
+ pass
952
1172
 
953
1173
  def _existing_file_matches(self, filepath: str, hashpath: str, expected_sha256: str | None) -> bool:
954
1174
  """Return whether a previously downloaded file is present and matches *expected_sha256*."""