saik0s commited on
Commit
33c25ce
·
verified ·
1 Parent(s): 8d5e143

Add files using upload-large-folder tool

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. custom_nodes/10S-Comfy-nodes/workflows/10Eros_10SNodes_I2V_v3_TiledSampler.json +1 -0
  2. custom_nodes/10S-Comfy-nodes/workflows/10Eros_10SNodes_LikenessGuideHelper_I2V_v3.2.json +0 -0
  3. custom_nodes/ComfyUI-GGUF/tools/README.md +93 -0
  4. custom_nodes/ComfyUI-GGUF/tools/convert.py +365 -0
  5. custom_nodes/ComfyUI-GGUF/tools/fix_5d_tensors.py +82 -0
  6. custom_nodes/ComfyUI-GGUF/tools/fix_lines_ending.py +31 -0
  7. custom_nodes/ComfyUI-GGUF/tools/lcpp.patch +451 -0
  8. custom_nodes/ComfyUI-GGUF/tools/read_tensors.py +21 -0
  9. custom_nodes/ComfyUI-HuggingFace/.github/workflows/publish.yml +28 -0
  10. custom_nodes/ComfyUI-HuggingFace/api/__init__.py +0 -0
  11. custom_nodes/ComfyUI-HuggingFace/api/huggingface.py +219 -0
  12. custom_nodes/ComfyUI-HuggingFace/downloader/__init__.py +0 -0
  13. custom_nodes/ComfyUI-HuggingFace/downloader/chunk_downloader.py +574 -0
  14. custom_nodes/ComfyUI-HuggingFace/downloader/manager.py +996 -0
  15. custom_nodes/ComfyUI-HuggingFace/server/__init__.py +5 -0
  16. custom_nodes/ComfyUI-HuggingFace/server/routes/CancelDownload.py +44 -0
  17. custom_nodes/ComfyUI-HuggingFace/server/routes/ClearHistory.py +32 -0
  18. custom_nodes/ComfyUI-HuggingFace/server/routes/DownloadModel.py +186 -0
  19. custom_nodes/ComfyUI-HuggingFace/server/routes/GetBaseModels.py +51 -0
  20. custom_nodes/ComfyUI-HuggingFace/server/routes/GetModelDetails.py +120 -0
  21. custom_nodes/ComfyUI-HuggingFace/server/routes/GetModelDirs.py +337 -0
  22. custom_nodes/ComfyUI-HuggingFace/server/routes/GetModelTypes.py +31 -0
  23. custom_nodes/ComfyUI-HuggingFace/server/routes/GetStatus.py +19 -0
  24. custom_nodes/ComfyUI-HuggingFace/server/routes/OpenPath.py +58 -0
  25. custom_nodes/ComfyUI-HuggingFace/server/routes/RetryDownload.py +39 -0
  26. custom_nodes/ComfyUI-HuggingFace/server/routes/SearchModels.py +121 -0
  27. custom_nodes/ComfyUI-HuggingFace/server/routes/__init__.py +21 -0
  28. custom_nodes/ComfyUI-HuggingFace/server/utils.py +187 -0
  29. custom_nodes/ComfyUI-HuggingFace/utils/__init__.py +15 -0
  30. custom_nodes/ComfyUI-HuggingFace/utils/helpers.py +224 -0
  31. custom_nodes/ComfyUI-HuggingFace/web/js/api/huggingface.js +164 -0
  32. custom_nodes/ComfyUI-HuggingFace/web/js/huggingfaceDownloader.css +753 -0
  33. custom_nodes/ComfyUI-HuggingFace/web/js/huggingfaceDownloader.js +89 -0
  34. custom_nodes/ComfyUI-HuggingFace/web/js/ui/UI.js +465 -0
  35. custom_nodes/ComfyUI-HuggingFace/web/js/ui/feedback.js +41 -0
  36. custom_nodes/ComfyUI-HuggingFace/web/js/ui/handlers/downloadHandler.js +106 -0
  37. custom_nodes/ComfyUI-HuggingFace/web/js/ui/handlers/eventListeners.js +248 -0
  38. custom_nodes/ComfyUI-HuggingFace/web/js/ui/handlers/searchHandler.js +39 -0
  39. custom_nodes/ComfyUI-HuggingFace/web/js/ui/handlers/settingsHandler.js +160 -0
  40. custom_nodes/ComfyUI-HuggingFace/web/js/ui/handlers/statusHandler.js +154 -0
  41. custom_nodes/ComfyUI-HuggingFace/web/js/ui/previewRenderer.js +96 -0
  42. custom_nodes/ComfyUI-HuggingFace/web/js/ui/searchRenderer.js +196 -0
  43. custom_nodes/ComfyUI-HuggingFace/web/js/ui/statusRenderer.js +124 -0
  44. custom_nodes/ComfyUI-HuggingFace/web/js/ui/templates.js +193 -0
  45. custom_nodes/ComfyUI-HuggingFace/web/js/utils/cookies.js +24 -0
  46. custom_nodes/ComfyUI-HuggingFace/web/js/utils/dom.js +32 -0
  47. custom_nodes/ComfyUI-KJNodes/.github/workflows/publish.yml +25 -0
  48. custom_nodes/ComfyUI-KJNodes/docs/images/319121566-05f66385-7568-4b1f-8bbc-11053660b02f.png +0 -0
  49. custom_nodes/ComfyUI-KJNodes/docs/images/319121636-706b5081-9120-4a29-bd76-901691ada688.png +0 -0
  50. custom_nodes/ComfyUI-KJNodes/example_workflows/leapfusion_hunyuuanvideo_i2v_native_testing.json +1188 -0
custom_nodes/10S-Comfy-nodes/workflows/10Eros_10SNodes_I2V_v3_TiledSampler.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"id":"ee686d30-b3a6-4a60-991e-b8bba9c03fd5","revision":0,"last_node_id":798,"last_link_id":2221,"nodes":[{"id":560,"type":"GetNode","pos":[207.38018918223796,3356.709215779959],"size":[210,58],"flags":{"collapsed":true},"order":0,"mode":0,"inputs":[],"outputs":[{"name":"FLOAT","type":"FLOAT","links":[1320]}],"title":"Get_fps_float","properties":{"Node name for S&R":"GetNode","aux_id":"kijai/ComfyUI-KJNodes","ue_properties":{"widget_ue_connectable":{},"version":"7.8","input_ue_unconnectable":{}}},"widgets_values":["fps_float"],"color":"#232","bgcolor":"#353"},{"id":546,"type":"GetNode","pos":[-148.71863287884347,4545.6150248054455],"size":[210,50],"flags":{"collapsed":true},"order":1,"mode":0,"inputs":[],"outputs":[{"name":"INT","type":"INT","links":[1297]}],"title":"Get_length_0","properties":{"Node name for S&R":"GetNode","aux_id":"kijai/ComfyUI-KJNodes","ue_properties":{"widget_ue_connectable":{},"version":"7.5.2","input_ue_unconnectable":{}}},"widgets_values":["length_0"],"color":"#1b4669","bgcolor":"#29699c"},{"id":630,"type":"SolidMask","pos":[1238.7248700088776,2874.9357428509056],"size":[270,106],"flags":{},"order":64,"mode":0,"inputs":[{"localized_name":"value","name":"value","type":"FLOAT","widget":{"name":"value"},"link":null},{"localized_name":"width","name":"width","type":"INT","widget":{"name":"width"},"link":1566},{"localized_name":"height","name":"height","type":"INT","widget":{"name":"height"},"link":1567}],"outputs":[{"localized_name":"MASK","name":"MASK","type":"MASK","links":[1457]}],"properties":{"cnr_id":"comfy-core","ver":"0.20.1","Node name for S&R":"SolidMask","ue_properties":{"widget_ue_connectable":{"value":true,"width":true,"height":true},"version":"7.8","input_ue_unconnectable":{}}},"widgets_values":[0,1024,1024]},{"id":279,"type":"GetNode","pos":[1254.5088394005425,3043.669001545407],"size":[210,58],"flags":{"collapsed":true},"order":2,"mode":0,"inputs":[],"outputs":[{"name":"VAE","type":"VAE","links":[586]}],"title":"Get_aud_vae","properties":{"Node name for S&R":"GetNode","aux_id":"kijai/ComfyUI-KJNodes","ue_properties":{"widget_ue_connectable":{},"version":"7.5.2","input_ue_unconnectable":{}}},"widgets_values":["aud_vae"],"color":"#322","bgcolor":"#533"},{"id":275,"type":"SetLatentNoiseMask","pos":[1491.3415734193702,3061.244993543124],"size":[236.13729359019888,46],"flags":{},"order":81,"mode":0,"inputs":[{"localized_name":"samples","name":"samples","type":"LATENT","link":583},{"localized_name":"mask","name":"mask","type":"MASK","link":1457}],"outputs":[{"localized_name":"LATENT","name":"LATENT","type":"LATENT","links":[]}],"properties":{"cnr_id":"comfy-core","ver":"0.9.2","Node name for S&R":"SetLatentNoiseMask","ue_properties":{"widget_ue_connectable":{},"version":"7.8","input_ue_unconnectable":{}}},"widgets_values":[]},{"id":525,"type":"RandomNoise","pos":[-137.47058558484093,3643.9027586613515],"size":[270,82],"flags":{"collapsed":true},"order":46,"mode":0,"inputs":[{"localized_name":"noise_seed","name":"noise_seed","type":"INT","widget":{"name":"noise_seed"},"link":1273}],"outputs":[{"localized_name":"NOISE","name":"NOISE","type":"NOISE","links":[1272]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.56","Node name for S&R":"RandomNoise","enableTabs":false,"tabWidth":65,"tabXOffset":10,"hasSecondTab":false,"secondTabText":"Send Back","secondTabOffset":80,"secondTabWidth":65,"ue_properties":{"widget_ue_connectable":{},"version":"7.5.2","input_ue_unconnectable":{}}},"widgets_values":[94345222367891,"randomize"]},{"id":526,"type":"SetNode","pos":[-143.90757904721147,3583.5426406626075],"size":[210,58],"flags":{"collapsed":true},"order":54,"mode":0,"inputs":[{"name":"NOISE","type":"NOISE","link":1272}],"outputs":[{"name":"NOISE","type":"NOISE","links":[1274]}],"title":"Set_noise_seed","properties":{"Node name for S&R":"SetNode","aux_id":"kijai/ComfyUI-KJNodes","previousName":"noise_seed","ue_properties":{"widget_ue_connectable":{},"version":"7.5.2","input_ue_unconnectable":{}}},"widgets_values":["noise_seed"],"color":"#2e2e2e","bgcolor":"#242121"},{"id":214,"type":"SetNode","pos":[-1141.6115413299842,3468.352336401272],"size":[210,60],"flags":{"collapsed":true},"order":42,"mode":0,"inputs":[{"name":"VAE","type":"VAE","link":1548}],"outputs":[{"name":"*","type":"*","links":null}],"title":"Set_vid_vae","properties":{"Node name for S&R":"SetNode","aux_id":"kijai/ComfyUI-KJNodes","previousName":"vid_vae","ue_properties":{"widget_ue_connectable":{},"version":"7.5.2","input_ue_unconnectable":{}}},"widgets_values":["vid_vae"],"color":"#322","bgcolor":"#533"},{"id":207,"type":"SetNode","pos":[-1156.2777177683586,3334.0638385561215],"size":[236.85766571079353,58],"flags":{"collapsed":true},"order":43,"mode":0,"inputs":[{"name":"VAE","type":"VAE","link":1423}],"outputs":[{"name":"*","type":"*","links":null}],"title":"Set_aud_vae","properties":{"Node name for S&R":"SetNode","aux_id":"kijai/ComfyUI-KJNodes","previousName":"aud_vae","ue_properties":{"widget_ue_connectable":{},"version":"7.5.2","input_ue_unconnectable":{}}},"widgets_values":["aud_vae"],"color":"#322","bgcolor":"#533"},{"id":552,"type":"VAEDecode","pos":[743.6726958514785,3808.798438402876],"size":[254.98011089086322,46],"flags":{},"order":75,"mode":0,"inputs":[{"localized_name":"samples","name":"samples","type":"LATENT","link":1905},{"localized_name":"vae","name":"vae","type":"VAE","link":1307}],"outputs":[{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","links":[1303]}],"properties":{"cnr_id":"comfy-core","ver":"0.9.2","Node name for S&R":"VAEDecode","ue_properties":{"widget_ue_connectable":{},"input_ue_unconnectable":{},"version":"7.5.2"}},"widgets_values":[]},{"id":652,"type":"Sigmas Easing","pos":[715.491530232057,4192.883029204863],"size":[447.6995362297288,154],"flags":{},"order":38,"mode":0,"inputs":[{"localized_name":"sigmas","name":"sigmas","type":"SIGMAS","link":1568},{"localized_name":"easing_type","name":"easing_type","type":"COMBO","widget":{"name":"easing_type"},"link":null},{"localized_name":"easing_mode","name":"easing_mode","type":"COMBO","widget":{"name":"easing_mode"},"link":null},{"localized_name":"normalize_input","name":"normalize_input","type":"BOOLEAN","widget":{"name":"normalize_input"},"link":null},{"localized_name":"normalize_output","name":"normalize_output","type":"BOOLEAN","widget":{"name":"normalize_output"},"link":null},{"localized_name":"strength","name":"strength","type":"FLOAT","widget":{"name":"strength"},"link":null}],"outputs":[{"localized_name":"SIGMAS","name":"SIGMAS","type":"SIGMAS","links":[1569]}],"title":"Sigmas Easing (cubic, sine, quad) don't mess with this tbh","properties":{"cnr_id":"RES4LYF","ver":"0dc91c00c4c3fb38e7874fcd7a2a327765e8882c","Node name for S&R":"Sigmas Easing","ue_properties":{"widget_ue_connectable":{"easing_type":true,"easing_mode":true,"normalize_input":true,"normalize_output":true,"strength":true},"version":"7.8","input_ue_unconnectable":{}}},"widgets_values":["cubic","in_out",true,true,0.7],"color":"#233","bgcolor":"#355"},{"id":551,"type":"AudioAdjustVolume","pos":[1102.859909498982,3854.587820408097],"size":[270,58],"flags":{},"order":77,"mode":0,"inputs":[{"localized_name":"audio","name":"audio","type":"AUDIO","link":1306},{"localized_name":"volume","name":"volume","type":"INT","widget":{"name":"volume"},"link":null}],"outputs":[{"localized_name":"AUDIO","name":"AUDIO","type":"AUDIO","links":[1714]}],"properties":{"cnr_id":"comfy-core","ver":"0.14.1","Node name for S&R":"AudioAdjustVolume","ue_properties":{"widget_ue_connectable":{"volume":true},"version":"7.8","input_ue_unconnectable":{}}},"widgets_values":[-3],"color":"#222","bgcolor":"#000"},{"id":535,"type":"LTXVEmptyLatentAudio","pos":[-147.71312885760986,4596.138881089034],"size":[270,120],"flags":{"collapsed":false},"order":37,"mode":0,"inputs":[{"localized_name":"audio_vae","name":"audio_vae","type":"VAE","link":1318},{"localized_name":"frames_number","name":"frames_number","type":"INT","widget":{"name":"frames_number"},"link":1295},{"localized_name":"frame_rate","name":"frame_rate","type":"INT","widget":{"name":"frame_rate"},"link":1296},{"localized_name":"batch_size","name":"batch_size","type":"INT","widget":{"name":"batch_size"},"link":null}],"outputs":[{"localized_name":"Latent","name":"Latent","type":"LATENT","links":[1609]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.68","Node name for S&R":"LTXVEmptyLatentAudio","enableTabs":false,"tabWidth":65,"tabXOffset":10,"hasSecondTab":false,"secondTabText":"Send Back","secondTabOffset":80,"secondTabWidth":65,"ue_properties":{"widget_ue_connectable":{"frames_number":true,"frame_rate":true},"version":"7.5.2","input_ue_unconnectable":{}}},"widgets_values":[1,24,1]},{"id":545,"type":"GetNode","pos":[-139.3191399780596,4787.418245887232],"size":[210,58],"flags":{"collapsed":true},"order":3,"mode":0,"inputs":[],"outputs":[{"name":"INT","type":"INT","links":[1296]}],"title":"Get_fps_int","properties":{"Node name for S&R":"GetNode","aux_id":"kijai/ComfyUI-KJNodes","ue_properties":{"widget_ue_connectable":{},"version":"7.8","input_ue_unconnectable":{}}},"widgets_values":["fps_int"],"color":"#1b4669","bgcolor":"#29699c"},{"id":543,"type":"GetNode","pos":[-136.65609065824208,4835.279017619837],"size":[210,50],"flags":{"collapsed":true},"order":4,"mode":0,"inputs":[],"outputs":[{"name":"INT","type":"INT","links":[1295]}],"title":"Get_length_0","properties":{"Node name for S&R":"GetNode","aux_id":"kijai/ComfyUI-KJNodes","ue_properties":{"widget_ue_connectable":{},"version":"7.5.2","input_ue_unconnectable":{}}},"widgets_values":["length_0"],"color":"#1b4669","bgcolor":"#29699c"},{"id":558,"type":"GetNode","pos":[-135.49266770428545,4892.598685074088],"size":[210,50],"flags":{"collapsed":true},"order":5,"mode":0,"inputs":[],"outputs":[{"name":"VAE","type":"VAE","links":[1318]}],"title":"Get_aud_vae","properties":{"Node name for S&R":"GetNode","aux_id":"kijai/ComfyUI-KJNodes","ue_properties":{"widget_ue_connectable":{},"version":"7.5.2","input_ue_unconnectable":{}}},"widgets_values":["aud_vae"],"color":"#322","bgcolor":"#533"},{"id":510,"type":"SamplerCustomAdvanced","pos":[744.2161311585023,3451.0839655295053],"size":[212.3638671875,106],"flags":{},"order":70,"mode":0,"inputs":[{"localized_name":"noise","name":"noise","type":"NOISE","link":1274},{"localized_name":"guider","name":"guider","type":"GUIDER","link":2043},{"localized_name":"sampler","name":"sampler","type":"SAMPLER","link":1276},{"localized_name":"sigmas","name":"sigmas","type":"SIGMAS","link":1569},{"localized_name":"latent_image","name":"latent_image","type":"LATENT","link":1531}],"outputs":[{"localized_name":"output","name":"output","type":"LATENT","links":[]},{"localized_name":"denoised_output","name":"denoised_output","type":"LATENT","links":[1546]}],"title":"First Pass","properties":{"cnr_id":"comfy-core","ver":"0.20.1","Node name for S&R":"SamplerCustomAdvanced","ue_properties":{"widget_ue_connectable":{},"version":"7.8","input_ue_unconnectable":{}}},"widgets_values":[],"color":"#323","bgcolor":"#535"},{"id":635,"type":"GetNode","pos":[3675.6779403363385,3944.3050892612646],"size":[210,58],"flags":{"collapsed":true},"order":6,"mode":0,"inputs":[],"outputs":[{"name":"FLOAT","type":"FLOAT","links":[1481]}],"title":"Get_fps_float","properties":{"Node name for S&R":"GetNode","aux_id":"kijai/ComfyUI-KJNodes","ue_properties":{"widget_ue_connectable":{},"version":"7.8","input_ue_unconnectable":{}}},"widgets_values":["fps_float"],"color":"#232","bgcolor":"#353"},{"id":714,"type":"GetNode","pos":[2813.360751805555,3608.779911647415],"size":[210,60],"flags":{"collapsed":false},"order":7,"mode":0,"inputs":[],"outputs":[{"name":"IMAGE","type":"IMAGE","links":[2107]}],"title":"Get_resized_image","properties":{"Node name for S&R":"GetNode","aux_id":"kijai/ComfyUI-KJNodes","ue_properties":{"widget_ue_connectable":{},"version":"7.8","input_ue_unconnectable":{}}},"widgets_values":["resized_image"],"color":"#2a363b","bgcolor":"#3f5159"},{"id":589,"type":"GetNode","pos":[2811.237769683004,3354.9545348795436],"size":[210,58],"flags":{"collapsed":false},"order":8,"mode":0,"inputs":[],"outputs":[{"name":"VAE","type":"VAE","links":[1957,2109]}],"title":"Get_vid_vae","properties":{"Node name for S&R":"GetNode","aux_id":"kijai/ComfyUI-KJNodes","ue_properties":{"widget_ue_connectable":{},"version":"7.5.2","input_ue_unconnectable":{}}},"widgets_values":["vid_vae"],"color":"#322","bgcolor":"#533"},{"id":548,"type":"LTXVConcatAVLatent","pos":[521.5234886410276,4088.4653662852597],"size":[270,46],"flags":{},"order":67,"mode":0,"inputs":[{"localized_name":"video_latent","name":"video_latent","type":"LATENT","link":2120},{"localized_name":"audio_latent","name":"audio_latent","type":"LATENT","link":1609}],"outputs":[{"localized_name":"latent","name":"latent","type":"LATENT","links":[1531]}],"properties":{"cnr_id":"comfy-core","ver":"0.7.0","Node name for S&R":"LTXVConcatAVLatent","enableTabs":false,"tabWidth":65,"tabXOffset":10,"hasSecondTab":false,"secondTabText":"Send Back","secondTabOffset":80,"secondTabWidth":65,"ue_properties":{"widget_ue_connectable":{},"version":"7.5.2","input_ue_unconnectable":{}}},"widgets_values":[],"color":"#432","bgcolor":"#653"},{"id":553,"type":"GetNode","pos":[742.4573892813876,3895.2825936892127],"size":[210,50],"flags":{"collapsed":true},"order":9,"mode":0,"inputs":[],"outputs":[{"name":"VAE","type":"VAE","links":[1307]}],"title":"Get_vid_vae","properties":{"Node name for S&R":"GetNode","aux_id":"kijai/ComfyUI-KJNodes","ue_properties":{"widget_ue_connectable":{},"version":"7.5.2","input_ue_unconnectable":{}}},"widgets_values":["vid_vae"],"color":"#322","bgcolor":"#533"},{"id":554,"type":"GetNode","pos":[742.707928003558,4040.7565523013463],"size":[210,34],"flags":{"collapsed":true},"order":10,"mode":0,"inputs":[],"outputs":[{"name":"VAE","type":"VAE","links":[1305]}],"title":"Get_aud_vae","properties":{"Node name for S&R":"GetNode","aux_id":"kijai/ComfyUI-KJNodes","ue_properties":{"widget_ue_connectable":{},"version":"7.5.2","input_ue_unconnectable":{}}},"widgets_values":["aud_vae"],"color":"#322","bgcolor":"#533"},{"id":331,"type":"SetNode","pos":[-383.2042149286154,4791.8832983604525],"size":[210,60],"flags":{"collapsed":true},"order":58,"mode":0,"inputs":[{"name":"IMAGE","type":"IMAGE","link":2106}],"outputs":[{"name":"*","type":"*","links":null}],"title":"Set_resized_image","properties":{"Node name for S&R":"SetNode","aux_id":"kijai/ComfyUI-KJNodes","previousName":"resized_image","ue_properties":{"widget_ue_connectable":{},"version":"7.8","input_ue_unconnectable":{}}},"widgets_values":["resized_image"],"color":"#2a363b","bgcolor":"#3f5159"},{"id":769,"type":"PreviewImage","pos":[-621.1482182304868,4877.562166931689],"size":[294.6088617022692,468.8946724679554],"flags":{},"order":57,"mode":0,"inputs":[{"localized_name":"images","name":"images","type":"IMAGE","link":2103}],"outputs":[],"properties":{"cnr_id":"comfy-core","ver":"0.20.1","Node name for S&R":"PreviewImage","ue_properties":{"widget_ue_connectable":{},"version":"7.8","input_ue_unconnectable":{}}},"widgets_values":[],"color":"#222","bgcolor":"#000"},{"id":653,"type":"STGGuiderAdvanced","pos":[489.975564844525,4419.025736243821],"size":[684.9663455547113,358],"flags":{"collapsed":false},"order":69,"mode":0,"inputs":[{"localized_name":"model","name":"model","type":"MODEL","link":1889},{"localized_name":"positive","name":"positive","type":"CONDITIONING","link":2144},{"localized_name":"negative","name":"negative","type":"CONDITIONING","link":2145},{"localized_name":"preset","name":"preset","shape":7,"type":"STG_ADVANCED_PRESET","link":null},{"localized_name":"skip_steps_sigma_threshold","name":"skip_steps_sigma_threshold","type":"FLOAT","widget":{"name":"skip_steps_sigma_threshold"},"link":null},{"localized_name":"cfg_star_rescale","name":"cfg_star_rescale","type":"BOOLEAN","widget":{"name":"cfg_star_rescale"},"link":null},{"localized_name":"sigmas","name":"sigmas","type":"STRING","widget":{"name":"sigmas"},"link":null},{"localized_name":"cfg_values","name":"cfg_values","type":"STRING","widget":{"name":"cfg_values"},"link":null},{"localized_name":"stg_scale_values","name":"stg_scale_values","type":"STRING","widget":{"name":"stg_scale_values"},"link":null},{"localized_name":"stg_rescale_values","name":"stg_rescale_values","type":"STRING","widget":{"name":"stg_rescale_values"},"link":null},{"localized_name":"stg_layers_indices","name":"stg_layers_indices","type":"STRING","widget":{"name":"stg_layers_indices"},"link":null},{"localized_name":"apply_apg","name":"apply_apg","shape":7,"type":"BOOLEAN","widget":{"name":"apply_apg"},"link":null},{"localized_name":"apg_cfg_scale","name":"apg_cfg_scale","shape":7,"type":"FLOAT","widget":{"name":"apg_cfg_scale"},"link":null},{"localized_name":"eta","name":"eta","shape":7,"type":"FLOAT","widget":{"name":"eta"},"link":null},{"localized_name":"norm_threshold","name":"norm_threshold","shape":7,"type":"FLOAT","widget":{"name":"norm_threshold"},"link":null}],"outputs":[{"localized_name":"GUIDER","name":"GUIDER","type":"GUIDER","links":[2043]}],"properties":{"cnr_id":"ComfyUI-LTXVideo","ver":"82bd963cdeb66d023bed8c99324a307020907ef8","Node name for S&R":"STGGuiderAdvanced","ue_properties":{"widget_ue_connectable":{"skip_steps_sigma_threshold":true,"cfg_star_rescale":true,"sigmas":true,"cfg_values":true,"stg_scale_values":true,"stg_rescale_values":true,"stg_layers_indices":true,"apply_apg":true,"apg_cfg_scale":true,"eta":true,"norm_threshold":true},"version":"7.8","input_ue_unconnectable":{}}},"widgets_values":[1,false,"1.0,0.99375,0.9875,0.98125,0.9550,0.8925,0.8120,0.7150,0.6030,0.4824,0.3618,0.2412,0.1206,0.0","2,1.5,1,1,1,1,1,1,1,1,1,1,1","2,1.5,1,1,1,1,1,1,1,1,1,1,1","1,1,1,1,1,1,1,1,1,1,1,1,1","[9999],[9999],[9999],[9999],[9999],[9999],[9999],[9999],[9999],[9999],[9999],[9999],[9999],[9999]",false,1,1,1],"color":"#233","bgcolor":"#355"},{"id":534,"type":"EmptyLTXVLatentVideo","pos":[-147.71695198347226,4316.913665791615],"size":[270,146.66666666666669],"flags":{},"order":63,"mode":0,"inputs":[{"localized_name":"width","name":"width","type":"INT","widget":{"name":"width"},"link":1289},{"localized_name":"height","name":"height","type":"INT","widget":{"name":"height"},"link":1290},{"localized_name":"length","name":"length","type":"INT","widget":{"name":"length"},"link":1297},{"localized_name":"batch_size","name":"batch_size","type":"INT","widget":{"name":"batch_size"},"link":null}],"outputs":[{"localized_name":"LATENT","name":"LATENT","type":"LATENT","links":[2117,2141]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.60","Node name for S&R":"EmptyLTXVLatentVideo","enableTabs":false,"tabWidth":65,"tabXOffset":10,"hasSecondTab":false,"secondTabText":"Send Back","secondTabOffset":80,"secondTabWidth":65,"ue_properties":{"widget_ue_connectable":{"width":true,"height":true,"length":true},"version":"7.5.2","input_ue_unconnectable":{}}},"widgets_values":[704,64,97,1]},{"id":527,"type":"ManualSigmas","pos":[170.64914217692814,3754.348551183275],"size":[279.41750632544984,59.3366153498655],"flags":{},"order":11,"mode":0,"inputs":[{"localized_name":"sigmas","name":"sigmas","type":"STRING","widget":{"name":"sigmas"},"link":null}],"outputs":[{"localized_name":"SIGMAS","name":"SIGMAS","type":"SIGMAS","links":[1568,1888]}],"title":"Sigmas First Pass","properties":{"cnr_id":"comfy-core","ver":"0.14.1","Node name for S&R":"ManualSigmas","enableTabs":false,"tabWidth":65,"tabXOffset":10,"hasSecondTab":false,"secondTabText":"Send Back","secondTabOffset":80,"secondTabWidth":65,"ue_properties":{"widget_ue_connectable":{},"version":"7.1","input_ue_unconnectable":{}}},"widgets_values":["1.0,0.99375,0.9875,0.98125,0.9550,0.8925,0.8120,0.7150,0.6030,0.4824,0.3618,0.2412,0.1206,0.0"],"color":"#323","bgcolor":"#535"},{"id":520,"type":"KSamplerSelect","pos":[181.6643151770866,3623.4807225016107],"size":[261.46963267468345,59.13737920167023],"flags":{},"order":12,"mode":0,"inputs":[{"localized_name":"sampler_name","name":"sampler_name","type":"COMBO","widget":{"name":"sampler_name"},"link":null}],"outputs":[{"localized_name":"SAMPLER","name":"SAMPLER","type":"SAMPLER","links":[1276]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.75","Node name for S&R":"KSamplerSelect","ue_properties":{"widget_ue_connectable":{"sampler_name":true},"version":"7.5.2","input_ue_unconnectable":{}},"enableTabs":false,"tabWidth":65,"tabXOffset":10,"hasSecondTab":false,"secondTabText":"Send Back","secondTabOffset":80,"secondTabWidth":65},"widgets_values":["euler_ancestral"],"color":"#233","bgcolor":"#355"},{"id":523,"type":"LTXVConditioning","pos":[179.95106180825883,3488.4504562735087],"size":[259.4071611759441,79.50641394128222],"flags":{"collapsed":false},"order":50,"mode":0,"inputs":[{"localized_name":"positive","name":"positive","type":"CONDITIONING","link":2142},{"localized_name":"negative","name":"negative","type":"CONDITIONING","link":2143},{"localized_name":"frame_rate","name":"frame_rate","type":"FLOAT","widget":{"name":"frame_rate"},"link":1320}],"outputs":[{"localized_name":"positive","name":"positive","type":"CONDITIONING","links":[2148]},{"localized_name":"negative","name":"negative","type":"CONDITIONING","links":[2149]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.56","Node name for S&R":"LTXVConditioning","ue_properties":{"widget_ue_connectable":{"frame_rate":true},"version":"7.5.2","input_ue_unconnectable":{}},"enableTabs":false,"tabWidth":65,"tabXOffset":10,"hasSecondTab":false,"secondTabText":"Send Back","secondTabOffset":80,"secondTabWidth":65},"widgets_values":[24],"color":"#233","bgcolor":"#355"},{"id":582,"type":"ManualSigmas","pos":[3512.1741421202964,3254.6564277665666],"size":[322.9496561279293,69.61597045898407],"flags":{},"order":13,"mode":0,"inputs":[{"localized_name":"sigmas","name":"sigmas","type":"STRING","widget":{"name":"sigmas"},"link":null}],"outputs":[{"localized_name":"SIGMAS","name":"SIGMAS","type":"SIGMAS","links":[2209]}],"title":"Sigmas Upscale","properties":{"cnr_id":"comfy-core","ver":"0.5.1","Node name for S&R":"ManualSigmas","enableTabs":false,"tabWidth":65,"tabXOffset":10,"hasSecondTab":false,"secondTabText":"Send Back","secondTabOffset":80,"secondTabWidth":65,"ue_properties":{"widget_ue_connectable":{},"version":"7.5.2","input_ue_unconnectable":{}}},"widgets_values":["0.85, 0.7250, 0.4219, 0.0"],"color":"#323","bgcolor":"#535"},{"id":585,"type":"KSamplerSelect","pos":[2813.2370757937415,3478.790375760474],"size":[270,58],"flags":{"collapsed":false},"order":14,"mode":0,"inputs":[{"localized_name":"sampler_name","name":"sampler_name","type":"COMBO","widget":{"name":"sampler_name"},"link":null}],"outputs":[{"localized_name":"SAMPLER","name":"SAMPLER","type":"SAMPLER","links":[2208]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.75","Node name for S&R":"KSamplerSelect","enableTabs":false,"tabWidth":65,"tabXOffset":10,"hasSecondTab":false,"secondTabText":"Send Back","secondTabOffset":80,"secondTabWidth":65,"ue_properties":{"widget_ue_connectable":{"sampler_name":true},"version":"7.8","input_ue_unconnectable":{}}},"widgets_values":["euler_ancestral_cfg_pp"],"color":"#322","bgcolor":"#533"},{"id":583,"type":"CFGGuider","pos":[3542.6365852942336,3663.4475380786234],"size":[270,98],"flags":{},"order":74,"mode":0,"inputs":[{"localized_name":"model","name":"model","type":"MODEL","link":2018},{"localized_name":"positive","name":"positive","type":"CONDITIONING","link":2137},{"localized_name":"negative","name":"negative","type":"CONDITIONING","link":2138},{"localized_name":"cfg","name":"cfg","type":"FLOAT","widget":{"name":"cfg"},"link":null}],"outputs":[{"localized_name":"GUIDER","name":"GUIDER","type":"GUIDER","links":[2207]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.71","Node name for S&R":"CFGGuider","enableTabs":false,"tabWidth":65,"tabXOffset":10,"hasSecondTab":false,"secondTabText":"Send Back","secondTabOffset":80,"secondTabWidth":65,"ue_properties":{"widget_ue_connectable":{},"version":"7.5.2","input_ue_unconnectable":{}}},"widgets_values":[1]},{"id":584,"type":"GetNode","pos":[3658.1883945443683,3604.7452337560367],"size":[210,50],"flags":{"collapsed":true},"order":15,"mode":0,"inputs":[],"outputs":[{"name":"NOISE","type":"NOISE","links":[2206]}],"title":"Get_noise_seed","properties":{"Node name for S&R":"GetNode","aux_id":"kijai/ComfyUI-KJNodes","ue_properties":{"widget_ue_connectable":{},"version":"7.5.2","input_ue_unconnectable":{}}},"widgets_values":["noise_seed"],"color":"#2e2e2e","bgcolor":"#242121"},{"id":550,"type":"LTXVAudioVAEDecode","pos":[736.2139899777825,3948.5308046878276],"size":[240,46],"flags":{},"order":73,"mode":0,"inputs":[{"localized_name":"samples","name":"samples","type":"LATENT","link":1608},{"label":"Audio VAE","localized_name":"audio_vae","name":"audio_vae","type":"VAE","link":1305}],"outputs":[{"localized_name":"Audio","name":"Audio","type":"AUDIO","links":[1306,1602]}],"properties":{"cnr_id":"comfy-core","ver":"0.7.0","Node name for S&R":"LTXVAudioVAEDecode","ue_properties":{"widget_ue_connectable":{},"version":"7.5.2","input_ue_unconnectable":{}},"enableTabs":false,"tabWidth":65,"tabXOffset":10,"hasSecondTab":false,"secondTabText":"Send Back","secondTabOffset":80,"secondTabWidth":65},"widgets_values":[]},{"id":596,"type":"LTXVSeparateAVLatent","pos":[4187.725994744947,3295.4824999445127],"size":[200.43247236282696,46],"flags":{},"order":85,"mode":0,"inputs":[{"localized_name":"av_latent","name":"av_latent","type":"LATENT","link":2211}],"outputs":[{"localized_name":"video_latent","name":"video_latent","type":"LATENT","links":[2172]},{"localized_name":"audio_latent","name":"audio_latent","type":"LATENT","links":[1611]}],"properties":{"cnr_id":"comfy-core","ver":"0.5.1","Node name for S&R":"LTXVSeparateAVLatent","enableTabs":false,"tabWidth":65,"tabXOffset":10,"hasSecondTab":false,"secondTabText":"Send Back","secondTabOffset":80,"secondTabWidth":65,"ue_properties":{"widget_ue_connectable":{},"version":"7.5.2","input_ue_unconnectable":{}}},"widgets_values":[],"color":"#432","bgcolor":"#653"},{"id":591,"type":"LTXVConcatAVLatent","pos":[3146.6765223307043,3745.3069037036903],"size":[270,46],"flags":{},"order":83,"mode":0,"inputs":[{"localized_name":"video_latent","name":"video_latent","type":"LATENT","link":2110},{"localized_name":"audio_latent","name":"audio_latent","type":"LATENT","link":2214}],"outputs":[{"localized_name":"latent","name":"latent","type":"LATENT","links":[2210]}],"properties":{"cnr_id":"comfy-core","ver":"0.5.1","Node name for S&R":"LTXVConcatAVLatent","enableTabs":false,"tabWidth":65,"tabXOffset":10,"hasSecondTab":false,"secondTabText":"Send Back","secondTabOffset":80,"secondTabWidth":65,"ue_properties":{"widget_ue_connectable":{},"version":"7.5.2","input_ue_unconnectable":{}}},"widgets_values":[]},{"id":770,"type":"LTXVImgToVideoInplaceKJ","pos":[3148.455220746667,3526.853708945063],"size":[294.98046875,146],"flags":{},"order":79,"mode":0,"inputs":[{"localized_name":"vae","name":"vae","type":"VAE","link":2109},{"localized_name":"latent","name":"latent","type":"LATENT","link":2122},{"localized_name":"num_images","name":"num_images","type":"COMFY_DYNAMICCOMBO_V3","widget":{"name":"num_images"},"link":null},{"localized_name":"num_images.strength_1","name":"num_images.strength_1","type":"FLOAT","widget":{"name":"num_images.strength_1"},"link":null},{"label":"image_1","localized_name":"num_images.image_1","name":"num_images.image_1","shape":7,"type":"IMAGE","link":2107},{"localized_name":"num_images.index_1","name":"num_images.index_1","shape":7,"type":"INT","widget":{"name":"num_images.index_1"},"link":null}],"outputs":[{"localized_name":"latent","name":"latent","type":"LATENT","links":[2110]}],"properties":{"cnr_id":"comfyui-kjnodes","ver":"a9ab19d2a7933e03f72bc00103863f06b9483300","Node name for S&R":"LTXVImgToVideoInplaceKJ","ue_properties":{"widget_ue_connectable":{"num_images":true,"num_images.strength_1":true,"num_images.index_1":true},"version":"7.8","input_ue_unconnectable":{}}},"widgets_values":["1",1,0],"color":"#233","bgcolor":"#355"},{"id":593,"type":"LTXVAudioVAEDecode","pos":[4186.736631469633,3668.359225683233],"size":[240,46],"flags":{},"order":87,"mode":0,"inputs":[{"localized_name":"samples","name":"samples","type":"LATENT","link":1611},{"label":"Audio VAE","localized_name":"audio_vae","name":"audio_vae","type":"VAE","link":1382}],"outputs":[{"localized_name":"Audio","name":"Audio","type":"AUDIO","links":[1387,2179]}],"properties":{"cnr_id":"comfy-core","ver":"0.7.0","Node name for S&R":"LTXVAudioVAEDecode","ue_properties":{"widget_ue_connectable":{},"version":"7.5.2","input_ue_unconnectable":{}},"enableTabs":false,"tabWidth":65,"tabXOffset":10,"hasSecondTab":false,"secondTabText":"Send Back","secondTabOffset":80,"secondTabWidth":65},"widgets_values":[]},{"id":783,"type":"easy loraNames","pos":[-947.4148190736797,3246.3912051532157],"size":[573.4679999999979,64.3888243713377],"flags":{},"order":16,"mode":0,"inputs":[{"localized_name":"lora_name","name":"lora_name","type":"COMBO","widget":{"name":"lora_name"},"link":null}],"outputs":[{"localized_name":"lora_name","name":"lora_name","type":"*","links":[2184,2185,2186,2187]}],"title":"Distilled Lora","properties":{"cnr_id":"comfyui-easy-use","ver":"1.3.5","Node name for S&R":"easy loraNames","ue_properties":{"widget_ue_connectable":{"lora_name":true},"version":"7.8","input_ue_unconnectable":{}}},"widgets_values":["ltx-2.3-22b-distilled-lora-1.1_fro90_ceil72_condsafe.safetensors"],"color":"#222","bgcolor":"#000"},{"id":780,"type":"JWStringToFloat","pos":[188.4736608306006,2933.8859567773025],"size":[270,58],"flags":{},"order":17,"mode":0,"inputs":[{"localized_name":"text","name":"text","type":"STRING","widget":{"name":"text"},"link":null}],"outputs":[{"localized_name":"FLOAT","name":"FLOAT","type":"FLOAT","links":[2181]}],"title":"First Pass Audio Strength","properties":{"cnr_id":"comfyui-various","ver":"5bd85aaf7616878471469c4ec7e11bbd0cef3bf2","Node name for S&R":"JWStringToFloat","ue_properties":{"widget_ue_connectable":{"text":true},"version":"7.8","input_ue_unconnectable":{}}},"widgets_values":["0.9"],"color":"#222","bgcolor":"#000"},{"id":782,"type":"JWStringToFloat","pos":[516.2577673613606,2935.444411519735],"size":[270,58],"flags":{},"order":18,"mode":0,"inputs":[{"localized_name":"text","name":"text","type":"STRING","widget":{"name":"text"},"link":null}],"outputs":[{"localized_name":"FLOAT","name":"FLOAT","type":"FLOAT","links":[2183]}],"title":"Upscale Video Strength","properties":{"cnr_id":"comfyui-various","ver":"5bd85aaf7616878471469c4ec7e11bbd0cef3bf2","Node name for S&R":"JWStringToFloat","ue_properties":{"widget_ue_connectable":{"text":true},"version":"7.8","input_ue_unconnectable":{}}},"widgets_values":["0.58"],"color":"#222","bgcolor":"#000"},{"id":594,"type":"GetNode","pos":[4017.8094594305244,3645.5361672191157],"size":[210,50],"flags":{"collapsed":true},"order":19,"mode":0,"inputs":[],"outputs":[{"name":"VAE","type":"VAE","links":[1942]}],"title":"Get_vid_vae","properties":{"Node name for S&R":"GetNode","aux_id":"kijai/ComfyUI-KJNodes","ue_properties":{"widget_ue_connectable":{},"version":"7.5.2","input_ue_unconnectable":{}}},"widgets_values":["vid_vae"],"color":"#322","bgcolor":"#533"},{"id":592,"type":"GetNode","pos":[3998.8089536312877,3815.052219814488],"size":[210,50],"flags":{"collapsed":true},"order":20,"mode":0,"inputs":[],"outputs":[{"name":"VAE","type":"VAE","links":[1382]}],"title":"Get_aud_vae","properties":{"Node name for S&R":"GetNode","aux_id":"kijai/ComfyUI-KJNodes","ue_properties":{"widget_ue_connectable":{},"version":"7.5.2","input_ue_unconnectable":{}}},"widgets_values":["aud_vae"],"color":"#322","bgcolor":"#533"},{"id":598,"type":"AudioAdjustVolume","pos":[3543.226714786005,3823.3257733842042],"size":[270,58],"flags":{},"order":89,"mode":0,"inputs":[{"localized_name":"audio","name":"audio","type":"AUDIO","link":1387},{"localized_name":"volume","name":"volume","type":"INT","widget":{"name":"volume"},"link":null}],"outputs":[{"localized_name":"AUDIO","name":"AUDIO","type":"AUDIO","links":[]}],"properties":{"cnr_id":"comfy-core","ver":"0.14.1","Node name for S&R":"AudioAdjustVolume","ue_properties":{"widget_ue_connectable":{"volume":true},"version":"7.8","input_ue_unconnectable":{}}},"widgets_values":[0],"color":"#222","bgcolor":"#000"},{"id":781,"type":"JWStringToFloat","pos":[838.2050706633592,2936.915769378011],"size":[270,58],"flags":{"collapsed":false},"order":21,"mode":0,"inputs":[{"localized_name":"text","name":"text","type":"STRING","widget":{"name":"text"},"link":null}],"outputs":[{"localized_name":"FLOAT","name":"FLOAT","type":"FLOAT","links":[2182]}],"title":"Upscale Audio Strength","properties":{"cnr_id":"comfyui-various","ver":"5bd85aaf7616878471469c4ec7e11bbd0cef3bf2","Node name for S&R":"JWStringToFloat","ue_properties":{"widget_ue_connectable":{"text":true},"version":"7.8","input_ue_unconnectable":{}}},"widgets_values":["1"],"color":"#222","bgcolor":"#000"},{"id":722,"type":"LTX2LoraLoaderAdvanced","pos":[-117.5080221723439,3066.4864860588896],"size":[293.6646484375,242],"flags":{"collapsed":true},"order":52,"mode":0,"inputs":[{"localized_name":"model","name":"model","type":"MODEL","link":1859},{"localized_name":"opt_lora_path","name":"opt_lora_path","shape":7,"type":"STRING","link":null},{"localized_name":"blocks","name":"blocks","shape":7,"type":"SELECTEDDITBLOCKS","link":null},{"localized_name":"lora_name","name":"lora_name","type":"COMBO","widget":{"name":"lora_name"},"link":2184},{"localized_name":"strength_model","name":"strength_model","type":"FLOAT","widget":{"name":"strength_model"},"link":2180},{"localized_name":"video","name":"video","type":"FLOAT","widget":{"name":"video"},"link":null},{"localized_name":"video_to_audio","name":"video_to_audio","type":"FLOAT","widget":{"name":"video_to_audio"},"link":null},{"localized_name":"audio","name":"audio","type":"FLOAT","widget":{"name":"audio"},"link":null},{"localized_name":"audio_to_video","name":"audio_to_video","type":"FLOAT","widget":{"name":"audio_to_video"},"link":null},{"localized_name":"other","name":"other","type":"FLOAT","widget":{"name":"other"},"link":null}],"outputs":[{"localized_name":"model","name":"model","type":"MODEL","links":[1860]},{"localized_name":"rank","name":"rank","type":"STRING","links":null},{"localized_name":"loaded_keys_info","name":"loaded_keys_info","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui-kjnodes","ver":"a9ab19d2a7933e03f72bc00103863f06b9483300","Node name for S&R":"LTX2LoraLoaderAdvanced","ue_properties":{"widget_ue_connectable":{"lora_name":true,"strength_model":true,"video":true,"video_to_audio":true,"audio":true,"audio_to_video":true,"other":true},"version":"7.8","input_ue_unconnectable":{}}},"widgets_values":["ltx23\\ltx-2.3-22b-distilled-lora-1.1_fro90_ceil72_condsafe.safetensors",1,1,0,0,0,1]},{"id":723,"type":"LTX2LoraLoaderAdvanced","pos":[201.42706107783408,3065.5746013380663],"size":[293.6646484375,242],"flags":{"collapsed":true},"order":60,"mode":0,"inputs":[{"localized_name":"model","name":"model","type":"MODEL","link":1860},{"localized_name":"opt_lora_path","name":"opt_lora_path","shape":7,"type":"STRING","link":null},{"localized_name":"blocks","name":"blocks","shape":7,"type":"SELECTEDDITBLOCKS","link":null},{"localized_name":"lora_name","name":"lora_name","type":"COMBO","widget":{"name":"lora_name"},"link":2185},{"localized_name":"strength_model","name":"strength_model","type":"FLOAT","widget":{"name":"strength_model"},"link":2181},{"localized_name":"video","name":"video","type":"FLOAT","widget":{"name":"video"},"link":null},{"localized_name":"video_to_audio","name":"video_to_audio","type":"FLOAT","widget":{"name":"video_to_audio"},"link":null},{"localized_name":"audio","name":"audio","type":"FLOAT","widget":{"name":"audio"},"link":null},{"localized_name":"audio_to_video","name":"audio_to_video","type":"FLOAT","widget":{"name":"audio_to_video"},"link":null},{"localized_name":"other","name":"other","type":"FLOAT","widget":{"name":"other"},"link":null}],"outputs":[{"localized_name":"model","name":"model","type":"MODEL","links":[2088]},{"localized_name":"rank","name":"rank","type":"STRING","links":null},{"localized_name":"loaded_keys_info","name":"loaded_keys_info","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui-kjnodes","ver":"a9ab19d2a7933e03f72bc00103863f06b9483300","Node name for S&R":"LTX2LoraLoaderAdvanced","ue_properties":{"widget_ue_connectable":{"lora_name":true,"strength_model":true,"video":true,"video_to_audio":true,"audio":true,"audio_to_video":true,"other":true},"version":"7.8","input_ue_unconnectable":{}}},"widgets_values":["ltx23\\ltx-2.3-22b-distilled-lora-1.1_fro90_ceil72_condsafe.safetensors",1,0,1,1,1,0]},{"id":719,"type":"LTX2LoraLoaderAdvanced","pos":[533.6158560203178,3067.449408361733],"size":[293.6646484375,242],"flags":{"collapsed":true},"order":51,"mode":0,"inputs":[{"localized_name":"model","name":"model","type":"MODEL","link":1852},{"localized_name":"opt_lora_path","name":"opt_lora_path","shape":7,"type":"STRING","link":null},{"localized_name":"blocks","name":"blocks","shape":7,"type":"SELECTEDDITBLOCKS","link":null},{"localized_name":"lora_name","name":"lora_name","type":"COMBO","widget":{"name":"lora_name"},"link":2186},{"localized_name":"strength_model","name":"strength_model","type":"FLOAT","widget":{"name":"strength_model"},"link":2183},{"localized_name":"video","name":"video","type":"FLOAT","widget":{"name":"video"},"link":null},{"localized_name":"video_to_audio","name":"video_to_audio","type":"FLOAT","widget":{"name":"video_to_audio"},"link":null},{"localized_name":"audio","name":"audio","type":"FLOAT","widget":{"name":"audio"},"link":null},{"localized_name":"audio_to_video","name":"audio_to_video","type":"FLOAT","widget":{"name":"audio_to_video"},"link":null},{"localized_name":"other","name":"other","type":"FLOAT","widget":{"name":"other"},"link":null}],"outputs":[{"localized_name":"model","name":"model","type":"MODEL","links":[1853]},{"localized_name":"rank","name":"rank","type":"STRING","links":null},{"localized_name":"loaded_keys_info","name":"loaded_keys_info","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui-kjnodes","ver":"a9ab19d2a7933e03f72bc00103863f06b9483300","Node name for S&R":"LTX2LoraLoaderAdvanced","ue_properties":{"widget_ue_connectable":{"lora_name":true,"strength_model":true,"video":true,"video_to_audio":true,"audio":true,"audio_to_video":true,"other":true},"version":"7.8","input_ue_unconnectable":{}}},"widgets_values":["ltx23\\ltx-2.3-22b-distilled-lora-1.1_fro90_ceil72_condsafe.safetensors",1,1,0,0,0,1]},{"id":718,"type":"LTX2LoraLoaderAdvanced","pos":[864.9976953461493,3064.0054448447786],"size":[293.6646484375,242],"flags":{"collapsed":true},"order":59,"mode":0,"inputs":[{"localized_name":"model","name":"model","type":"MODEL","link":1853},{"localized_name":"opt_lora_path","name":"opt_lora_path","shape":7,"type":"STRING","link":null},{"localized_name":"blocks","name":"blocks","shape":7,"type":"SELECTEDDITBLOCKS","link":null},{"localized_name":"lora_name","name":"lora_name","type":"COMBO","widget":{"name":"lora_name"},"link":2187},{"localized_name":"strength_model","name":"strength_model","type":"FLOAT","widget":{"name":"strength_model"},"link":2182},{"localized_name":"video","name":"video","type":"FLOAT","widget":{"name":"video"},"link":null},{"localized_name":"video_to_audio","name":"video_to_audio","type":"FLOAT","widget":{"name":"video_to_audio"},"link":null},{"localized_name":"audio","name":"audio","type":"FLOAT","widget":{"name":"audio"},"link":null},{"localized_name":"audio_to_video","name":"audio_to_video","type":"FLOAT","widget":{"name":"audio_to_video"},"link":null},{"localized_name":"other","name":"other","type":"FLOAT","widget":{"name":"other"},"link":null}],"outputs":[{"localized_name":"model","name":"model","type":"MODEL","links":[2017]},{"localized_name":"rank","name":"rank","type":"STRING","links":null},{"localized_name":"loaded_keys_info","name":"loaded_keys_info","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui-kjnodes","ver":"a9ab19d2a7933e03f72bc00103863f06b9483300","Node name for S&R":"LTX2LoraLoaderAdvanced","ue_properties":{"widget_ue_connectable":{"lora_name":true,"strength_model":true,"video":true,"video_to_audio":true,"audio":true,"audio_to_video":true,"other":true},"version":"7.8","input_ue_unconnectable":{}}},"widgets_values":["ltx23\\ltx-2.3-22b-distilled-lora-1.1_fro90_ceil72_condsafe.safetensors",1,0,1,1,1,0]},{"id":743,"type":"LatentUpscaleModelLoader","pos":[2742.801612317037,3240.087891289923],"size":[353.96248779296855,58],"flags":{},"order":22,"mode":0,"inputs":[{"localized_name":"model_name","name":"model_name","type":"COMBO","widget":{"name":"model_name"},"link":null}],"outputs":[{"localized_name":"LATENT_UPSCALE_MODEL","name":"LATENT_UPSCALE_MODEL","type":"LATENT_UPSCALE_MODEL","links":[1956]}],"properties":{"cnr_id":"comfy-core","ver":"0.20.1","Node name for S&R":"LatentUpscaleModelLoader","ue_properties":{"widget_ue_connectable":{"model_name":true},"version":"7.8","input_ue_unconnectable":{}}},"widgets_values":["ltx-2.3-spatial-upscaler-x2-1.1.safetensors"],"color":"#323","bgcolor":"#535"},{"id":537,"type":"CLIPTextEncode","pos":[-636.294950541902,4074.23542703082],"size":[397.87115230398985,118.93963289899375],"flags":{"collapsed":false},"order":40,"mode":0,"inputs":[{"localized_name":"clip","name":"clip","type":"CLIP","link":1420},{"localized_name":"text","name":"text","type":"STRING","widget":{"name":"text"},"link":null}],"outputs":[{"localized_name":"CONDITIONING","name":"CONDITIONING","type":"CONDITIONING","links":[2143]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.56","Node name for S&R":"CLIPTextEncode","enableTabs":false,"tabWidth":65,"tabXOffset":10,"hasSecondTab":false,"secondTabText":"Send Back","secondTabOffset":80,"secondTabWidth":65,"ue_properties":{"widget_ue_connectable":{},"version":"7.5.2","input_ue_unconnectable":{}}},"widgets_values":["captions, music, transition, VR, bad quality, subtitles, text, watermark, overlay effects, pc game, yelling, console game, video game, cartoon, childish, ugly, text, blur, logo, wordmark, static, low quality, noise, white noise, bleep, censoring, censor, bleeping, beep, beeping, newscast, interview, podcast, mutant, horror, 70's, film grain, cinematic, comedy, stand-up "],"color":"#322","bgcolor":"#533"},{"id":559,"type":"GetNode","pos":[-459.24751170881257,4260.730291995303],"size":[210,58],"flags":{"collapsed":true},"order":23,"mode":0,"inputs":[],"outputs":[{"name":"VAE","type":"VAE","links":[1886,2075,2118]}],"title":"Get_vid_vae","properties":{"Node name for S&R":"GetNode","aux_id":"kijai/ComfyUI-KJNodes","ue_properties":{"widget_ue_connectable":{},"version":"7.5.2","input_ue_unconnectable":{}}},"widgets_values":["vid_vae"],"color":"#322","bgcolor":"#533"},{"id":755,"type":"RTXVideoSuperResolution","pos":[5042.307315358794,3574.972988633893],"size":[285.81015625,106],"flags":{},"order":88,"mode":0,"inputs":[{"localized_name":"images","name":"images","type":"IMAGE","link":2025},{"localized_name":"resize_type","name":"resize_type","type":"COMFY_DYNAMICCOMBO_V3","widget":{"name":"resize_type"},"link":null},{"localized_name":"resize_type.scale","name":"resize_type.scale","type":"FLOAT","widget":{"name":"resize_type.scale"},"link":null},{"localized_name":"quality","name":"quality","type":"COMBO","widget":{"name":"quality"},"link":null}],"outputs":[{"localized_name":"upscaled_images","name":"upscaled_images","type":"IMAGE","links":[2026]}],"properties":{"cnr_id":"comfyui_nvidia_rtx_nodes","ver":"892515e3eb9a4920a131a502a047e47adca9eb0d","Node name for S&R":"RTXVideoSuperResolution"},"widgets_values":["scale by multiplier",1.3,"HIGH"]},{"id":274,"type":"LTXVAudioVAEEncode","pos":[1547.6286442388596,2949.1333592283954],"size":[201.4126953125,46],"flags":{},"order":78,"mode":0,"inputs":[{"localized_name":"audio","name":"audio","type":"AUDIO","link":1602},{"label":"Audio VAE","localized_name":"audio_vae","name":"audio_vae","type":"VAE","link":586}],"outputs":[{"localized_name":"Audio Latent","name":"Audio Latent","type":"LATENT","links":[583,2212]}],"properties":{"cnr_id":"comfy-core","ver":"0.9.2","Node name for S&R":"LTXVAudioVAEEncode","ue_properties":{"widget_ue_connectable":{},"version":"7.8","input_ue_unconnectable":{}}},"widgets_values":[]},{"id":556,"type":"LTXVSeparateAVLatent","pos":[743.8788106962104,3691.3292693154235],"size":[244.39530248670872,46],"flags":{},"order":71,"mode":0,"inputs":[{"localized_name":"av_latent","name":"av_latent","type":"LATENT","link":1546}],"outputs":[{"localized_name":"video_latent","name":"video_latent","type":"LATENT","links":[1900]},{"localized_name":"audio_latent","name":"audio_latent","type":"LATENT","links":[1608,2213]}],"properties":{"cnr_id":"comfy-core","ver":"0.5.1","Node name for S&R":"LTXVSeparateAVLatent","enableTabs":false,"tabWidth":65,"tabXOffset":10,"hasSecondTab":false,"secondTabText":"Send Back","secondTabOffset":80,"secondTabWidth":65,"ue_properties":{"widget_ue_connectable":{},"version":"7.5.2","input_ue_unconnectable":{}}},"widgets_values":[],"color":"#432","bgcolor":"#653"},{"id":789,"type":"TwoWaySwitch","pos":[2100.2422289843566,3308.3526900407155],"size":[474.2561723693839,115.75201477050769],"flags":{"collapsed":false},"order":82,"mode":0,"inputs":[{"localized_name":"input_1","name":"input_1","shape":7,"type":"*","link":2212},{"localized_name":"input_2","name":"input_2","shape":7,"type":"*","link":2213},{"localized_name":"selection_setting","name":"selection_setting","type":"INT","widget":{"name":"selection_setting"},"link":null}],"outputs":[{"localized_name":"output","name":"output","type":"*","links":[2214]}],"title":"Resample Audio (2) or Force First Pass Audio (1)","properties":{"cnr_id":"controlaltai-nodes","ver":"721492b66c9cede8ae23ae10615462ad80cfd061","Node name for S&R":"TwoWaySwitch","ue_properties":{"widget_ue_connectable":{"selection_setting":true},"version":"7.8","input_ue_unconnectable":{}}},"widgets_values":[2],"color":"#222","bgcolor":"#000"},{"id":753,"type":"LTXTextAttentionAmplifier","pos":[3146.764295574803,3858.251464179244],"size":[307.0767578125,154],"flags":{},"order":62,"mode":0,"inputs":[{"localized_name":"model","name":"model","type":"MODEL","link":2017},{"localized_name":"text_amplification","name":"text_amplification","shape":7,"type":"FLOAT","widget":{"name":"text_amplification"},"link":null},{"localized_name":"spatial_focus","name":"spatial_focus","shape":7,"type":"FLOAT","widget":{"name":"spatial_focus"},"link":null},{"localized_name":"block_index_filter","name":"block_index_filter","shape":7,"type":"STRING","widget":{"name":"block_index_filter"},"link":null},{"localized_name":"bypass","name":"bypass","shape":7,"type":"BOOLEAN","widget":{"name":"bypass"},"link":null},{"localized_name":"debug","name":"debug","shape":7,"type":"BOOLEAN","widget":{"name":"debug"},"link":null}],"outputs":[{"localized_name":"model","name":"model","type":"MODEL","links":[2018]}],"properties":{"aux_id":"TenStrip/10S-Comfy-nodes","ver":"47852934037fbf96ed6b6572dded3c1da37bf36d","Node name for S&R":"LTXTextAttentionAmplifier"},"widgets_values":[1.3,0.15000000000000002,"36-48",false,false],"color":"#223","bgcolor":"#335"},{"id":616,"type":"LTXAVTextEncoderLoader","pos":[-1484.9909480082615,3537.073274708929],"size":[318.444921875,106],"flags":{},"order":24,"mode":0,"inputs":[{"localized_name":"text_encoder","name":"text_encoder","type":"COMBO","widget":{"name":"text_encoder"},"link":null},{"localized_name":"ckpt_name","name":"ckpt_name","type":"COMBO","widget":{"name":"ckpt_name"},"link":null},{"localized_name":"device","name":"device","type":"COMBO","widget":{"name":"device"},"link":null}],"outputs":[{"localized_name":"CLIP","name":"CLIP","type":"CLIP","links":[1418,1419,1420]}],"properties":{"cnr_id":"comfy-core","ver":"0.20.1","Node name for S&R":"LTXAVTextEncoderLoader","ue_properties":{"widget_ue_connectable":{"text_encoder":true,"ckpt_name":true,"device":true},"version":"7.8","input_ue_unconnectable":{}}},"widgets_values":["gemma_3_12B_it_fp8_e4m3fn.safetensors","10Eros_v1-fp8mixed_learned.safetensors","default"],"color":"#222","bgcolor":"#000"},{"id":646,"type":"CheckpointLoaderSimple","pos":[-1482.6914004943567,3359.3040731716023],"size":[270,98],"flags":{},"order":25,"mode":0,"inputs":[{"localized_name":"ckpt_name","name":"ckpt_name","type":"COMBO","widget":{"name":"ckpt_name"},"link":null}],"outputs":[{"localized_name":"MODEL","name":"MODEL","type":"MODEL","links":[1547]},{"localized_name":"CLIP","name":"CLIP","type":"CLIP","links":null},{"localized_name":"VAE","name":"VAE","type":"VAE","links":[1548]}],"properties":{"cnr_id":"comfy-core","ver":"0.20.1","Node name for S&R":"CheckpointLoaderSimple","ue_properties":{"widget_ue_connectable":{"ckpt_name":true},"version":"7.8","input_ue_unconnectable":{}}},"widgets_values":["10Eros_v1-fp8mixed_learned.safetensors"],"color":"#432","bgcolor":"#653"},{"id":617,"type":"LTXVAudioVAELoader","pos":[-1480.7914338617873,3235.5379033147087],"size":[270,58],"flags":{},"order":26,"mode":0,"inputs":[{"localized_name":"ckpt_name","name":"ckpt_name","type":"COMBO","widget":{"name":"ckpt_name"},"link":null}],"outputs":[{"localized_name":"Audio VAE","name":"Audio VAE","type":"VAE","links":[1423]}],"properties":{"cnr_id":"comfy-core","ver":"0.20.1","Node name for S&R":"LTXVAudioVAELoader","ue_properties":{"widget_ue_connectable":{"ckpt_name":true},"version":"7.8","input_ue_unconnectable":{}}},"widgets_values":["10Eros_v1-fp8mixed_learned.safetensors"],"color":"#332922","bgcolor":"#593930"},{"id":531,"type":"ImageResizeKJv2","pos":[-623.0137075112468,4404.1052439928335],"size":[288.23754794013735,336.00000000000045],"flags":{"collapsed":true},"order":49,"mode":0,"inputs":[{"localized_name":"image","name":"image","type":"IMAGE","link":2150},{"localized_name":"mask","name":"mask","shape":7,"type":"MASK","link":null},{"localized_name":"width","name":"width","type":"INT","widget":{"name":"width"},"link":2215},{"localized_name":"height","name":"height","type":"INT","widget":{"name":"height"},"link":2216},{"localized_name":"upscale_method","name":"upscale_method","type":"COMBO","widget":{"name":"upscale_method"},"link":null},{"localized_name":"keep_proportion","name":"keep_proportion","type":"COMBO","widget":{"name":"keep_proportion"},"link":null},{"localized_name":"pad_color","name":"pad_color","type":"STRING","widget":{"name":"pad_color"},"link":null},{"localized_name":"crop_position","name":"crop_position","type":"COMBO","widget":{"name":"crop_position"},"link":null},{"localized_name":"divisible_by","name":"divisible_by","type":"INT","widget":{"name":"divisible_by"},"link":null},{"localized_name":"device","name":"device","shape":7,"type":"COMBO","widget":{"name":"device"},"link":null}],"outputs":[{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","links":[1281,2103,2106,2111,2139,2140]},{"localized_name":"width","name":"width","type":"INT","links":null},{"localized_name":"height","name":"height","type":"INT","links":null},{"localized_name":"mask","name":"mask","type":"MASK","links":null}],"properties":{"cnr_id":"comfyui-kjnodes","ver":"4dfb85dcc52e4315c33170d97bb987baa46d128b","Node name for S&R":"ImageResizeKJv2","ue_properties":{"widget_ue_connectable":{},"version":"7.5.2","input_ue_unconnectable":{}}},"widgets_values":[0,0,"area","crop","0, 0, 0","center",32,"cpu"],"color":"#232","bgcolor":"#353"},{"id":532,"type":"ResizeImageMaskNode","pos":[-627.975883287854,4495.453864775762],"size":[270,106],"flags":{},"order":56,"mode":0,"inputs":[{"localized_name":"input","name":"input","type":"IMAGE,MASK","link":1281},{"localized_name":"resize_type","name":"resize_type","type":"COMFY_DYNAMICCOMBO_V3","widget":{"name":"resize_type"},"link":null},{"localized_name":"resize_type.multiplier","name":"resize_type.multiplier","type":"FLOAT","widget":{"name":"resize_type.multiplier"},"link":null},{"localized_name":"scale_method","name":"scale_method","type":"COMBO","widget":{"name":"scale_method"},"link":null}],"outputs":[{"localized_name":"resized","name":"resized","type":"IMAGE","links":[1282]}],"properties":{"cnr_id":"comfy-core","ver":"0.14.1","Node name for S&R":"ResizeImageMaskNode","ue_properties":{"widget_ue_connectable":{"resize_type":true,"resize_type.multiplier":true,"scale_method":true},"version":"7.8","input_ue_unconnectable":{}}},"widgets_values":["scale by multiplier",0.5,"area"]},{"id":533,"type":"GetImageSize","pos":[-625.7563863146385,4658.162256891575],"size":[210,136],"flags":{"collapsed":true},"order":61,"mode":0,"inputs":[{"localized_name":"image","name":"image","type":"IMAGE","link":1282}],"outputs":[{"localized_name":"width","name":"width","type":"INT","links":[1289,1566]},{"localized_name":"height","name":"height","type":"INT","links":[1290,1567]},{"localized_name":"batch_size","name":"batch_size","type":"INT","links":null}],"properties":{"cnr_id":"comfy-core","ver":"0.7.0","Node name for S&R":"GetImageSize","ue_properties":{"widget_ue_connectable":{},"version":"7.5.2","input_ue_unconnectable":{}}},"widgets_values":[],"color":"#232","bgcolor":"#353"},{"id":621,"type":"SetNode","pos":[-982.6473011877084,4117.627386747715],"size":[210,58],"flags":{"collapsed":true},"order":47,"mode":0,"inputs":[{"name":"INT","type":"INT","link":1426}],"outputs":[{"name":"*","type":"*","links":null}],"title":"Set_seed","properties":{"Node name for S&R":"SetNode","aux_id":"kijai/ComfyUI-KJNodes","previousName":"seed","ue_properties":{"widget_ue_connectable":{},"version":"7.8","input_ue_unconnectable":{}}},"widgets_values":["seed"],"color":"#1b4669","bgcolor":"#29699c"},{"id":539,"type":"SetNode","pos":[-1350.4852103468006,3976.131691041333],"size":[210,50],"flags":{"collapsed":true},"order":53,"mode":0,"inputs":[{"name":"INT","type":"INT","link":1291}],"outputs":[{"name":"*","type":"*","links":null}],"title":"Set_fps_int","properties":{"Node name for S&R":"SetNode","aux_id":"kijai/ComfyUI-KJNodes","previousName":"fps_int","ue_properties":{"widget_ue_connectable":{},"version":"7.5.2","input_ue_unconnectable":{}}},"widgets_values":["fps_int"],"color":"#1b4669","bgcolor":"#29699c"},{"id":540,"type":"CM_FloatToInt","pos":[-1353.334898876357,3979.7395006294246],"size":[269.31637824001507,60.0508913584531],"flags":{"collapsed":true},"order":44,"mode":0,"inputs":[{"localized_name":"a","name":"a","type":"FLOAT","widget":{"name":"a"},"link":1292}],"outputs":[{"localized_name":"INT","name":"INT","type":"INT","links":[1291]}],"properties":{"cnr_id":"ComfyMath","ver":"c01177221c31b8e5fbc062778fc8254aeb541638","Node name for S&R":"CM_FloatToInt","ue_properties":{"widget_ue_connectable":{"a":true},"version":"7.8","input_ue_unconnectable":{}}},"widgets_values":[0]},{"id":541,"type":"SetNode","pos":[-1349.0391107707837,3972.7422031848832],"size":[210,58],"flags":{"collapsed":true},"order":45,"mode":0,"inputs":[{"name":"FLOAT","type":"FLOAT","link":1293}],"outputs":[{"name":"*","type":"*","links":null}],"title":"Set_fps_float","properties":{"Node name for S&R":"SetNode","aux_id":"kijai/ComfyUI-KJNodes","previousName":"fps_float","ue_properties":{"widget_ue_connectable":{},"version":"7.5.2","input_ue_unconnectable":{}}},"widgets_values":["fps_float"],"color":"#232","bgcolor":"#353"},{"id":597,"type":"VHS_VideoCombine","pos":[3989.618639202434,3886.685868295037],"size":[1682.5545248780247,2108.35184987085],"flags":{"collapsed":false},"order":90,"mode":0,"inputs":[{"localized_name":"images","name":"images","type":"IMAGE","link":2026},{"localized_name":"audio","name":"audio","shape":7,"type":"AUDIO","link":2179},{"localized_name":"meta_batch","name":"meta_batch","shape":7,"type":"VHS_BatchManager","link":null},{"localized_name":"vae","name":"vae","shape":7,"type":"VAE","link":null},{"localized_name":"frame_rate","name":"frame_rate","type":"FLOAT","widget":{"name":"frame_rate"},"link":1481},{"localized_name":"loop_count","name":"loop_count","type":"INT","widget":{"name":"loop_count"},"link":null},{"localized_name":"filename_prefix","name":"filename_prefix","type":"STRING","widget":{"name":"filename_prefix"},"link":null},{"localized_name":"format","name":"format","type":"COMBO","widget":{"name":"format"},"link":null},{"localized_name":"pingpong","name":"pingpong","type":"BOOLEAN","widget":{"name":"pingpong"},"link":null},{"localized_name":"save_output","name":"save_output","type":"BOOLEAN","widget":{"name":"save_output"},"link":null},{"name":"pix_fmt","type":["yuv420p","yuv420p10le"],"widget":{"name":"pix_fmt"},"link":null},{"name":"crf","type":"INT","widget":{"name":"crf"},"link":null},{"name":"save_metadata","type":"BOOLEAN","widget":{"name":"save_metadata"},"link":null},{"name":"trim_to_audio","type":"BOOLEAN","widget":{"name":"trim_to_audio"},"link":null}],"outputs":[{"localized_name":"Filenames","name":"Filenames","type":"VHS_FILENAMES","links":null}],"properties":{"cnr_id":"comfyui-videohelpersuite","ver":"1.7.9","Node name for S&R":"VHS_VideoCombine","ue_properties":{"widget_ue_connectable":{"frame_rate":true,"loop_count":true,"filename_prefix":true,"format":true,"pingpong":true,"save_output":true,"pix_fmt":true,"crf":true,"save_metadata":true,"trim_to_audio":true},"version":"7.8","input_ue_unconnectable":{}}},"widgets_values":{"frame_rate":24,"loop_count":0,"filename_prefix":"Eros/I2V","format":"video/h264-mp4","pix_fmt":"yuv420p","crf":10,"save_metadata":true,"trim_to_audio":false,"pingpong":false,"save_output":true,"videopreview":{"hidden":false,"paused":false,"params":{"filename":"I2V_00013-audio.mp4","subfolder":"Eros","type":"output","format":"video/h264-mp4","frame_rate":25,"workflow":"I2V_00013.png","fullpath":"H:\\New_AI_hub\\matrix2\\Data\\Packages\\ComfyUI2\\output\\Eros\\I2V_00013-audio.mp4"}}},"color":"#222","bgcolor":"#000"},{"id":740,"type":"VAEDecode","pos":[4193.035737094888,3532.5951130775347],"size":[254.98011089086322,46],"flags":{},"order":86,"mode":0,"inputs":[{"localized_name":"samples","name":"samples","type":"LATENT","link":2172},{"localized_name":"vae","name":"vae","type":"VAE","link":1942}],"outputs":[{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","links":[2025]}],"properties":{"cnr_id":"comfy-core","ver":"0.9.2","Node name for S&R":"VAEDecode","ue_properties":{"widget_ue_connectable":{},"input_ue_unconnectable":{},"version":"7.5.2"}},"widgets_values":[]},{"id":549,"type":"VHS_VideoCombine","pos":[1381.9752964545542,4109.515776559602],"size":[805.7524473679769,1428.4096151309682],"flags":{"collapsed":false},"order":80,"mode":0,"inputs":[{"localized_name":"images","name":"images","type":"IMAGE","link":1303},{"localized_name":"audio","name":"audio","shape":7,"type":"AUDIO","link":1714},{"localized_name":"meta_batch","name":"meta_batch","shape":7,"type":"VHS_BatchManager","link":null},{"localized_name":"vae","name":"vae","shape":7,"type":"VAE","link":null},{"localized_name":"frame_rate","name":"frame_rate","type":"FLOAT","widget":{"name":"frame_rate"},"link":1308},{"localized_name":"loop_count","name":"loop_count","type":"INT","widget":{"name":"loop_count"},"link":null},{"localized_name":"filename_prefix","name":"filename_prefix","type":"STRING","widget":{"name":"filename_prefix"},"link":null},{"localized_name":"format","name":"format","type":"COMBO","widget":{"name":"format"},"link":null},{"localized_name":"pingpong","name":"pingpong","type":"BOOLEAN","widget":{"name":"pingpong"},"link":null},{"localized_name":"save_output","name":"save_output","type":"BOOLEAN","widget":{"name":"save_output"},"link":null},{"name":"pix_fmt","type":["yuv420p","yuv420p10le"],"widget":{"name":"pix_fmt"},"link":null},{"name":"crf","type":"INT","widget":{"name":"crf"},"link":null},{"name":"save_metadata","type":"BOOLEAN","widget":{"name":"save_metadata"},"link":null},{"name":"trim_to_audio","type":"BOOLEAN","widget":{"name":"trim_to_audio"},"link":null}],"outputs":[{"localized_name":"Filenames","name":"Filenames","type":"VHS_FILENAMES","links":null}],"properties":{"cnr_id":"comfyui-videohelpersuite","ver":"1.7.9","Node name for S&R":"VHS_VideoCombine","ue_properties":{"widget_ue_connectable":{"frame_rate":true,"loop_count":true,"filename_prefix":true,"format":true,"pingpong":true,"save_output":true,"pix_fmt":true,"crf":true,"save_metadata":true,"trim_to_audio":true},"version":"7.8","input_ue_unconnectable":{}}},"widgets_values":{"frame_rate":1,"loop_count":0,"filename_prefix":"10E_firstpass","format":"video/h264-mp4","pix_fmt":"yuv420p","crf":8,"save_metadata":true,"trim_to_audio":false,"pingpong":false,"save_output":false,"videopreview":{"hidden":false,"paused":false,"params":{"filename":"10E_firstpass_00018-audio.mp4","subfolder":"","type":"temp","format":"video/h264-mp4","frame_rate":25,"workflow":"10E_firstpass_00018.png","fullpath":"H:\\New_AI_hub\\matrix2\\Data\\Packages\\ComfyUI2\\temp\\10E_firstpass_00018-audio.mp4"}}},"color":"#222","bgcolor":"#000"},{"id":744,"type":"LTXVLatentUpsamplerTiled","pos":[3144.4283806312183,3251.3863845050637],"size":[315.929296875,194],"flags":{},"order":76,"mode":0,"inputs":[{"localized_name":"samples","name":"samples","type":"LATENT","link":2091},{"localized_name":"upscale_model","name":"upscale_model","type":"LATENT_UPSCALE_MODEL","link":1956},{"localized_name":"vae","name":"vae","type":"VAE","link":1957},{"localized_name":"tile_size","name":"tile_size","shape":7,"type":"INT","widget":{"name":"tile_size"},"link":null},{"localized_name":"overlap","name":"overlap","shape":7,"type":"INT","widget":{"name":"overlap"},"link":null},{"localized_name":"max_size_for_no_tile","name":"max_size_for_no_tile","shape":7,"type":"INT","widget":{"name":"max_size_for_no_tile"},"link":null},{"localized_name":"rotate_for_landscape","name":"rotate_for_landscape","shape":7,"type":"BOOLEAN","widget":{"name":"rotate_for_landscape"},"link":null},{"localized_name":"debug","name":"debug","shape":7,"type":"BOOLEAN","widget":{"name":"debug"},"link":null}],"outputs":[{"localized_name":"LATENT","name":"LATENT","type":"LATENT","links":[2122]}],"properties":{"aux_id":"TenStrip/10S-Comfy-nodes","ver":"47852934037fbf96ed6b6572dded3c1da37bf36d","Node name for S&R":"LTXVLatentUpsamplerTiled"},"widgets_values":[11,6,22,false,false],"color":"#223","bgcolor":"#335"},{"id":555,"type":"GetNode","pos":[1132.0161214120128,4089.5184760428774],"size":[210,58],"flags":{"collapsed":true},"order":27,"mode":0,"inputs":[],"outputs":[{"name":"FLOAT","type":"FLOAT","links":[1308]}],"title":"Get_fps_float","properties":{"Node name for S&R":"GetNode","aux_id":"kijai/ComfyUI-KJNodes","ue_properties":{"widget_ue_connectable":{},"version":"7.8","input_ue_unconnectable":{}}},"widgets_values":["fps_float"],"color":"#232","bgcolor":"#353"},{"id":734,"type":"LTXVCropGuides","pos":[1115.4955143216073,3969.1883218852795],"size":[152.7134765625,66],"flags":{},"order":72,"mode":0,"inputs":[{"localized_name":"positive","name":"positive","type":"CONDITIONING","link":2146},{"localized_name":"negative","name":"negative","type":"CONDITIONING","link":2147},{"localized_name":"latent","name":"latent","type":"LATENT","link":1900}],"outputs":[{"localized_name":"positive","name":"positive","type":"CONDITIONING","links":[2137]},{"localized_name":"negative","name":"negative","type":"CONDITIONING","links":[2138]},{"localized_name":"latent","name":"latent","type":"LATENT","links":[1905,2091]}],"properties":{"cnr_id":"comfy-core","ver":"0.20.1","Node name for S&R":"LTXVCropGuides","ue_properties":{"widget_ue_connectable":{},"version":"7.8","input_ue_unconnectable":{}}},"widgets_values":[]},{"id":779,"type":"JWStringToFloat","pos":[-131.4793794648068,2931.8241735329775],"size":[270,58],"flags":{},"order":28,"mode":0,"inputs":[{"localized_name":"text","name":"text","type":"STRING","widget":{"name":"text"},"link":null}],"outputs":[{"localized_name":"FLOAT","name":"FLOAT","type":"FLOAT","links":[2180]}],"title":"First Pass Video Strength","properties":{"cnr_id":"comfyui-various","ver":"5bd85aaf7616878471469c4ec7e11bbd0cef3bf2","Node name for S&R":"JWStringToFloat","ue_properties":{"widget_ue_connectable":{"text":true},"version":"7.8","input_ue_unconnectable":{}}},"widgets_values":["0.88"],"color":"#222","bgcolor":"#000"},{"id":772,"type":"LTXVImgToVideoInplaceKJ","pos":[176.24606920810962,4085.756030025743],"size":[294.98046875,146],"flags":{},"order":65,"mode":0,"inputs":[{"localized_name":"vae","name":"vae","type":"VAE","link":2118},{"localized_name":"latent","name":"latent","type":"LATENT","link":2117},{"localized_name":"num_images","name":"num_images","type":"COMFY_DYNAMICCOMBO_V3","widget":{"name":"num_images"},"link":null},{"localized_name":"num_images.strength_1","name":"num_images.strength_1","type":"FLOAT","widget":{"name":"num_images.strength_1"},"link":2219},{"label":"image_1","localized_name":"num_images.image_1","name":"num_images.image_1","shape":7,"type":"IMAGE","link":2140},{"localized_name":"num_images.index_1","name":"num_images.index_1","shape":7,"type":"INT","widget":{"name":"num_images.index_1"},"link":null}],"outputs":[{"localized_name":"latent","name":"latent","type":"LATENT","links":[2120,2123]}],"properties":{"cnr_id":"comfyui-kjnodes","ver":"a9ab19d2a7933e03f72bc00103863f06b9483300","Node name for S&R":"LTXVImgToVideoInplaceKJ","ue_properties":{"widget_ue_connectable":{"num_images":true,"num_images.strength_1":true,"num_images.index_1":true},"version":"7.8","input_ue_unconnectable":{}}},"widgets_values":["1",0.7499999999999999,0],"color":"#233","bgcolor":"#355"},{"id":767,"type":"LTXVAddGuide","pos":[177.01976308882737,3864.7547451448645],"size":[270,162],"flags":{},"order":68,"mode":0,"inputs":[{"localized_name":"positive","name":"positive","type":"CONDITIONING","link":2148},{"localized_name":"negative","name":"negative","type":"CONDITIONING","link":2149},{"localized_name":"vae","name":"vae","type":"VAE","link":2075},{"localized_name":"latent","name":"latent","type":"LATENT","link":2123},{"localized_name":"image","name":"image","type":"IMAGE","link":2139},{"localized_name":"frame_idx","name":"frame_idx","type":"INT","widget":{"name":"frame_idx"},"link":null},{"localized_name":"strength","name":"strength","type":"FLOAT","widget":{"name":"strength"},"link":2218}],"outputs":[{"localized_name":"positive","name":"positive","type":"CONDITIONING","links":[2144,2146]},{"localized_name":"negative","name":"negative","type":"CONDITIONING","links":[2145,2147]},{"localized_name":"latent","name":"latent","type":"LATENT","links":[]}],"properties":{"cnr_id":"comfy-core","ver":"0.20.1","Node name for S&R":"LTXVAddGuide","ue_properties":{"widget_ue_connectable":{"frame_idx":true,"strength":true},"version":"7.8","input_ue_unconnectable":{}}},"widgets_values":[0,0.7499999999999999],"color":"#233","bgcolor":"#355"},{"id":542,"type":"PrimitiveFloat","pos":[-1425.8431702584294,3927.973226571728],"size":[263.24,58],"flags":{},"order":29,"mode":0,"inputs":[{"localized_name":"value","name":"value","type":"FLOAT","widget":{"name":"value"},"link":null}],"outputs":[{"localized_name":"FLOAT","name":"FLOAT","type":"FLOAT","links":[1292,1293]}],"title":"Base Frame Rate (24 Default)","properties":{"cnr_id":"comfy-core","ver":"0.8.0","Node name for S&R":"PrimitiveFloat","ue_properties":{"widget_ue_connectable":{},"version":"7.5.2","input_ue_unconnectable":{}}},"widgets_values":[25],"color":"#222","bgcolor":"#000"},{"id":788,"type":"LTXTiledSampler","pos":[3875.1742002272445,3304.8312860366577],"size":[279.60546875,282],"flags":{},"order":84,"mode":0,"inputs":[{"localized_name":"noise","name":"noise","type":"NOISE","link":2206},{"localized_name":"guider","name":"guider","type":"GUIDER","link":2207},{"localized_name":"sampler","name":"sampler","type":"SAMPLER","link":2208},{"localized_name":"sigmas","name":"sigmas","type":"SIGMAS","link":2209},{"localized_name":"latent_image","name":"latent_image","type":"LATENT","link":2210},{"localized_name":"tile_axis","name":"tile_axis","shape":7,"type":"COMBO","widget":{"name":"tile_axis"},"link":null},{"localized_name":"n_tiles","name":"n_tiles","shape":7,"type":"INT","widget":{"name":"n_tiles"},"link":null},{"localized_name":"tile_overlap","name":"tile_overlap","shape":7,"type":"INT","widget":{"name":"tile_overlap"},"link":null},{"localized_name":"max_size_for_no_tile","name":"max_size_for_no_tile","shape":7,"type":"INT","widget":{"name":"max_size_for_no_tile"},"link":null},{"localized_name":"audio_pass","name":"audio_pass","shape":7,"type":"COMBO","widget":{"name":"audio_pass"},"link":null},{"localized_name":"audio_carrier_tile","name":"audio_carrier_tile","shape":7,"type":"COMBO","widget":{"name":"audio_carrier_tile"},"link":null},{"localized_name":"debug","name":"debug","shape":7,"type":"BOOLEAN","widget":{"name":"debug"},"link":null}],"outputs":[{"localized_name":"output","name":"output","type":"LATENT","links":null},{"localized_name":"denoised_output","name":"denoised_output","type":"LATENT","links":[2211]}],"properties":{"aux_id":"TenStrip/10S-Comfy-nodes","ver":"47852934037fbf96ed6b6572dded3c1da37bf36d","Node name for S&R":"LTXTiledSampler"},"widgets_values":["auto",2,4,38,"tile_carrying","first",false],"color":"#223","bgcolor":"#335"},{"id":557,"type":"Power Lora Loader (rgthree)","pos":[-796.6583203205506,3371.2238770967456],"size":[428.9642467678492,322.75598817305604],"flags":{},"order":41,"mode":0,"inputs":[{"dir":3,"name":"model","type":"MODEL","link":1547},{"dir":3,"name":"clip","type":"CLIP","link":1418}],"outputs":[{"dir":4,"name":"MODEL","shape":3,"type":"MODEL","links":[1852,1859]},{"dir":4,"name":"CLIP","shape":3,"type":"CLIP","links":null}],"properties":{"cnr_id":"rgthree-comfy","ver":"1.0.2512112053","Show Strengths":"Single Strength","Match":"","ue_properties":{"widget_ue_connectable":{},"version":"7.8","input_ue_unconnectable":{}}},"widgets_values":[{},{"type":"PowerLoraLoaderHeaderWidget"},{},""],"color":"#222","bgcolor":"#000"},{"id":797,"type":"mxSlider","pos":[-1291.7110040938053,4233.142465026225],"size":[329.41939288068033,30],"flags":{"collapsed":false},"order":31,"mode":0,"inputs":[{"localized_name":"Xi","name":"Xi","type":"INT","widget":{"name":"Xi"},"link":null},{"localized_name":"Xf","name":"Xf","type":"FLOAT","widget":{"name":"Xf"},"link":null},{"localized_name":"isfloatX","name":"isfloatX","type":"INT","widget":{"name":"isfloatX"},"link":null}],"outputs":[{"localized_name":"","name":"","type":"FLOAT","links":[2218,2219]}],"title":"Conditioning Strength","properties":{"cnr_id":"comfyui-mxtoolkit","ver":"0.9.92","Node name for S&R":"mxSlider","value":0.8,"min":0.4,"max":1,"step":0.05,"decimals":2,"snap":true,"ue_properties":{"version":"7.1","widget_ue_connectable":{},"input_ue_unconnectable":{}},"aux_id":"Smirnov75/ComfyUI-mxToolkit"},"widgets_values":[0,0.8,1],"color":"#222","bgcolor":"#000"},{"id":538,"type":"SetNode","pos":[-1040.509645734978,3950.804887433771],"size":[210,50],"flags":{"collapsed":true},"order":55,"mode":0,"inputs":[{"name":"INT","type":"INT","link":2221}],"outputs":[{"name":"*","type":"*","links":null}],"title":"Set_length","properties":{"Node name for S&R":"SetNode","aux_id":"kijai/ComfyUI-KJNodes","previousName":"","ue_properties":{"widget_ue_connectable":{},"version":"7.5.2","input_ue_unconnectable":{}}},"widgets_values":["length_0"],"color":"#1b4669","bgcolor":"#29699c"},{"id":798,"type":"ComfyMathExpression","pos":[-1053.6205311505564,3975.8234801246163],"size":[400,200],"flags":{"collapsed":true},"order":48,"mode":0,"inputs":[{"label":"a","localized_name":"values.a","name":"values.a","type":"FLOAT,INT","link":2220},{"label":"b","localized_name":"values.b","name":"values.b","shape":7,"type":"FLOAT,INT","link":null},{"localized_name":"expression","name":"expression","type":"STRING","widget":{"name":"expression"},"link":null}],"outputs":[{"localized_name":"FLOAT","name":"FLOAT","type":"FLOAT","links":null},{"localized_name":"INT","name":"INT","type":"INT","links":[2221]}],"properties":{"cnr_id":"comfy-core","ver":"0.20.1","Node name for S&R":"ComfyMathExpression"},"widgets_values":["a + 1"]},{"id":791,"type":"mxSlider","pos":[-1432.9324238695663,4053.537250181444],"size":[280.438609128239,30],"flags":{"collapsed":false},"order":33,"mode":0,"inputs":[{"localized_name":"Xi","name":"Xi","type":"INT","widget":{"name":"Xi"},"link":null},{"localized_name":"Xf","name":"Xf","type":"FLOAT","widget":{"name":"Xf"},"link":null},{"localized_name":"isfloatX","name":"isfloatX","type":"INT","widget":{"name":"isfloatX"},"link":null}],"outputs":[{"localized_name":"","name":"","type":"INT","links":[2215]}],"title":"Video Width","properties":{"cnr_id":"comfyui-mxtoolkit","ver":"0.9.92","Node name for S&R":"mxSlider","value":1024,"min":480,"max":2400,"step":32,"decimals":0,"snap":true,"ue_properties":{"version":"7.1","widget_ue_connectable":{},"input_ue_unconnectable":{}},"aux_id":"Smirnov75/ComfyUI-mxToolkit"},"widgets_values":[1024,1024,0],"color":"#222","bgcolor":"#000"},{"id":773,"type":"LoadImage","pos":[-1429.4456334516165,4356.670409703162],"size":[646.0397431410997,996.8304004758456],"flags":{},"order":34,"mode":0,"inputs":[{"localized_name":"image","name":"image","type":"COMBO","widget":{"name":"image"},"link":null},{"localized_name":"choose file to upload","name":"upload","type":"IMAGEUPLOAD","widget":{"name":"upload"},"link":null}],"outputs":[{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","links":[2150]},{"localized_name":"MASK","name":"MASK","type":"MASK","links":null}],"properties":{"cnr_id":"comfy-core","ver":"0.20.1","Node name for S&R":"LoadImage","ue_properties":{"widget_ue_connectable":{"image":true,"upload":true},"version":"7.8","input_ue_unconnectable":{}}},"widgets_values":["pasted/image (97).png","image"],"color":"#222","bgcolor":"#000"},{"id":731,"type":"LTXLatentAnchorAware","pos":[-155.21856858203535,3792.1885062578012],"size":[290.362890625,450],"flags":{},"order":66,"mode":0,"inputs":[{"localized_name":"model","name":"model","type":"MODEL","link":2088},{"localized_name":"reference_image","name":"reference_image","shape":7,"type":"IMAGE","link":2111},{"localized_name":"vae","name":"vae","shape":7,"type":"VAE","link":1886},{"localized_name":"energy_latent","name":"energy_latent","shape":7,"type":"LATENT","link":2141},{"localized_name":"sigmas","name":"sigmas","shape":7,"type":"SIGMAS","link":1888},{"localized_name":"strength","name":"strength","shape":7,"type":"FLOAT","widget":{"name":"strength"},"link":null},{"localized_name":"cache_at_step","name":"cache_at_step","shape":7,"type":"INT","widget":{"name":"cache_at_step"},"link":null},{"localized_name":"similarity_threshold","name":"similarity_threshold","shape":7,"type":"FLOAT","widget":{"name":"similarity_threshold"},"link":null},{"localized_name":"decay_with_distance","name":"decay_with_distance","shape":7,"type":"FLOAT","widget":{"name":"decay_with_distance"},"link":null},{"localized_name":"energy_threshold","name":"energy_threshold","shape":7,"type":"FLOAT","widget":{"name":"energy_threshold"},"link":null},{"localized_name":"bypass","name":"bypass","shape":7,"type":"BOOLEAN","widget":{"name":"bypass"},"link":null},{"localized_name":"debug","name":"debug","shape":7,"type":"BOOLEAN","widget":{"name":"debug"},"link":null},{"localized_name":"advanced_mode","name":"advanced_mode","shape":7,"type":"BOOLEAN","widget":{"name":"advanced_mode"},"link":null},{"localized_name":"cache_mode","name":"cache_mode","shape":7,"type":"COMBO","widget":{"name":"cache_mode"},"link":null},{"localized_name":"forwards_per_step","name":"forwards_per_step","shape":7,"type":"INT","widget":{"name":"forwards_per_step"},"link":null},{"localized_name":"cache_warmup","name":"cache_warmup","shape":7,"type":"INT","widget":{"name":"cache_warmup"},"link":null},{"localized_name":"anchor_frame","name":"anchor_frame","shape":7,"type":"INT","widget":{"name":"anchor_frame"},"link":null},{"localized_name":"depth_curve","name":"depth_curve","shape":7,"type":"COMBO","widget":{"name":"depth_curve"},"link":null},{"localized_name":"block_index_filter","name":"block_index_filter","shape":7,"type":"STRING","widget":{"name":"block_index_filter"},"link":null}],"outputs":[{"localized_name":"model","name":"model","type":"MODEL","links":[1889]}],"properties":{"aux_id":"TenStrip/10S-Comfy-nodes","ver":"47852934037fbf96ed6b6572dded3c1da37bf36d","Node name for S&R":"LTXLatentAnchorAware"},"widgets_values":[0.11000000000000001,6,0.5,0.15000000000000002,0.3,false,false,true,"schedule",1,432,0,"flat","10-30"],"color":"#223","bgcolor":"#335"},{"id":795,"type":"MarkdownNote","pos":[-2021.1608937808342,3197.3792734479475],"size":[521.3413669628835,1161.0100683552218],"flags":{},"order":30,"mode":0,"inputs":[],"outputs":[],"properties":{"ue_properties":{"widget_ue_connectable":{},"version":"7.8","input_ue_unconnectable":{}}},"widgets_values":["# 10S Workflows\n\nThese are advanced quality-focused conditioned workflows for 10Eros/Sulphur/Base LTX 2.3. They make some compromises on speed for quality. \nSteal these or study 10S Node intergation if you are a workflow maker or want to customize and release them as your own. \n**They are NOT FAST.** I find with LTX, fast ≠ good.\n\n---\n\n## 📦 Downloads\n\n| Resource | Link |\n|----------|------|\n| 🎬 Workflows | [LTX2.3-10Eros Workflows](https://huggingface.co/TenStrip/LTX2.3-10Eros_Workflows) |\n| 🧠 Models (10Eros/Sulphur) | [huggingface.co/TenStrip/LTX2.3-10Eros](https://huggingface.co/TenStrip/LTX2.3-10Eros) |\n| 🔧 10S Comfy Nodes | [TenStrip/10S-Comfy-nodes](https://github.com/TenStrip/10S-Comfy-nodes) |\n| ⚗️ Distilled LoRAs | [LTX2.3 Distilled LoRA 1.1 Experiments](https://huggingface.co/TenStrip/LTX2.3_Distilled_Lora_1.1_Experiments/tree/main) |\n---\n\n## ⚙️ How It Works\n\nThese workflows rely on **[10S-Comfy-nodes]**\n\nGuided prompt conditioning from the guide is leveraged against the i2v conditioned latent with Latent anchor. Increasing strength on Latent anchor as well as **depth_curve** gives you a more manageable control over less/more movement and gives you 3 points of strength to control the i2v freedom. \n\nLatent Anchor node needs paper-level study in what it does, but for most calculations turn debug on and follow this formula to set it up correctly: \n*target_call_count = cache_at_step × forwards_per_step*\n\nIt is currently configured for 13 steps at step 6 cache with 1 CFG, although forwards per step must be increased to 3 to account for CFG,STG,audioCFG-but isn't impactful on just first 2 CFG steps.\n\nUpscale distortion is removed and detail is increased with tiled sampler, this is a default-level improvement to quality. At it's current setting it will only trigger when distortion inducing resolution is reached for vertical ratios, if working in landscape increase max_tile_size accordingly to bypass it. Audio-sync issues can arise if the **tile_carrying** mode does not target the tile with talking heads or sound-causing actors. Generally you will select first for vertical resolutions and middle for huge landscape passes.\nBecause of how tiled sampler works you will want to avoid hand motion and movement near the middle of the composition.\n\nDistillation is handled at full break down. Lower pass video distillation will increase motion. Upscale pass video distillation will add detail, but also add artifacts and unaesthetic blur artifacts. Audio weight must be kept high for audio quality.\n\nIn some cases first pass audio may be more desirable, that can be toggled to be hard encoded instead of passed and resampled fully.\n\nThe first pass output is a full **motion** preview, if it contains mutation, distortion, bad motion, bad audio, etc... The upscale pass will not fix it. Only allow it to run to upscale when first pass motion looks good. **Visual quality and style will be restored** at upscale.\n\n\n---\n\n## ✍️ Prompt Enhancement\n\nThese use STG Guider and have CFG for initial steps which is where most broad motion and audio form. **Your negative prompt is impactful, so use it.**\n\nFor best positive prompt results, use this foreword in **Grok** or an **uncensored LLM**:\n\n---\n\n*Generate a video scene script with a description based on the attached image for an LLM that has a tokenizer that uses interleaved attention to support long-context understanding that is fed into a multimodal video model. Strict specification, follow up to the word: No timestamps. No unnecessary embellishment. Output only plain English text and make it a copy box.*\n\n*First, describe the image initial scene in concise natural language; subject(s), subject(s) appearance, subject(s) composition and pose, background, and context.*\n\n*Next, formulate a naturally evolving scenario that would take place describing every moving body part, composition change, and manipulation from the uploaded initial frame that would be reflected in the video models post-latent evolution output. If the image is explicit or sexual in nature, use full anatomical terminology and spice it up slightly with visually representable erotic themes.*\n\n*Center the prompt around this basic idea: **[ concept ]***\n\n*Interweave this dialogue or sound concept into the scene with descriptions of voice tone followed by the lines delivered in quotations, in a temporal sequence between or during motions. Dialogue should be concise and non-rambling as it will take away from video quality: **[ dialogue ]***\n\n*Inside that prompt describe only notable audio and audio cues, both normal and explicit; background noise as well as foley and natural sounds. In a temporal sequence paired with coinciding motions. In the case of absent dialogue or soundscapes and only if background music is fitting; describe a fitting genre and melodic tone with matching mood.*\n\n*Output only text following above instruction. Follow-up suggestions should be on the topic of expanding or changing motion or dialogue from the output text.*\n"],"color":"#432","bgcolor":"#653"},{"id":536,"type":"CLIPTextEncode","pos":[-641.7618745880603,3760.1840791968625],"size":[394.7031737883649,255.53167788014707],"flags":{},"order":39,"mode":0,"inputs":[{"localized_name":"clip","name":"clip","type":"CLIP","link":1419},{"localized_name":"text","name":"text","type":"STRING","widget":{"name":"text"},"link":null}],"outputs":[{"localized_name":"CONDITIONING","name":"CONDITIONING","type":"CONDITIONING","links":[2142]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.56","Node name for S&R":"CLIPTextEncode","enableTabs":false,"tabWidth":65,"tabXOffset":10,"hasSecondTab":false,"secondTabText":"Send Back","secondTabOffset":80,"secondTabWidth":65,"ue_properties":{"widget_ue_connectable":{},"version":"7.5.2","input_ue_unconnectable":{}}},"widgets_values":[""],"color":"#232","bgcolor":"#353"},{"id":796,"type":"mxSlider","pos":[-1140.5320997743233,3944.5899083019813],"size":[329.41939288068033,30],"flags":{"collapsed":false},"order":35,"mode":0,"inputs":[{"localized_name":"Xi","name":"Xi","type":"INT","widget":{"name":"Xi"},"link":null},{"localized_name":"Xf","name":"Xf","type":"FLOAT","widget":{"name":"Xf"},"link":null},{"localized_name":"isfloatX","name":"isfloatX","type":"INT","widget":{"name":"isfloatX"},"link":null}],"outputs":[{"localized_name":"","name":"","type":"INT","links":[2220]}],"title":"Length (Frame Count X/24 = Seconds)","properties":{"cnr_id":"comfyui-mxtoolkit","ver":"0.9.92","Node name for S&R":"mxSlider","value":264,"min":48,"max":960,"step":24,"decimals":0,"snap":true,"ue_properties":{"version":"7.1","widget_ue_connectable":{},"input_ue_unconnectable":{}},"aux_id":"Smirnov75/ComfyUI-mxToolkit"},"widgets_values":[264,264,0],"color":"#222","bgcolor":"#000"},{"id":524,"type":"Seed (rgthree)","pos":[-1122.836985011658,4031.7241836848057],"size":[288.55371900826447,130],"flags":{},"order":32,"mode":0,"inputs":[],"outputs":[{"dir":4,"name":"SEED","shape":3,"type":"INT","links":[1273,1426]}],"properties":{"cnr_id":"rgthree-comfy","ver":"1.0.2511270846","randomMax":1125899906842624,"randomMin":0,"ue_properties":{"widget_ue_connectable":{},"version":"7.8","input_ue_unconnectable":{}}},"widgets_values":[-1,"","",""],"color":"#222","bgcolor":"#000"},{"id":792,"type":"mxSlider","pos":[-1434.642031574845,4150.46662371214],"size":[280.438609128239,30],"flags":{"collapsed":false},"order":36,"mode":0,"inputs":[{"localized_name":"Xi","name":"Xi","type":"INT","widget":{"name":"Xi"},"link":null},{"localized_name":"Xf","name":"Xf","type":"FLOAT","widget":{"name":"Xf"},"link":null},{"localized_name":"isfloatX","name":"isfloatX","type":"INT","widget":{"name":"isfloatX"},"link":null}],"outputs":[{"localized_name":"","name":"","type":"INT","links":[2216]}],"title":"Video Height","properties":{"cnr_id":"comfyui-mxtoolkit","ver":"0.9.92","Node name for S&R":"mxSlider","value":1344,"min":480,"max":2400,"step":32,"decimals":0,"snap":true,"ue_properties":{"version":"7.1","widget_ue_connectable":{},"input_ue_unconnectable":{}},"aux_id":"Smirnov75/ComfyUI-mxToolkit"},"widgets_values":[1344,1344,0],"color":"#222","bgcolor":"#000"}],"links":[[583,274,0,275,0,"LATENT"],[586,279,0,274,1,"VAE"],[1272,525,0,526,0,"NOISE"],[1273,524,0,525,0,"INT"],[1274,526,0,510,0,"NOISE"],[1276,520,0,510,2,"SAMPLER"],[1281,531,0,532,0,"IMAGE"],[1282,532,0,533,0,"IMAGE"],[1289,533,0,534,0,"INT"],[1290,533,1,534,1,"INT"],[1291,540,0,539,0,"INT"],[1292,542,0,540,0,"FLOAT"],[1293,542,0,541,0,"FLOAT"],[1295,543,0,535,1,"INT"],[1296,545,0,535,2,"INT"],[1297,546,0,534,2,"INT"],[1303,552,0,549,0,"IMAGE"],[1305,554,0,550,1,"VAE"],[1306,550,0,551,0,"AUDIO"],[1307,553,0,552,1,"VAE"],[1308,555,0,549,4,"FLOAT"],[1318,558,0,535,0,"VAE"],[1320,560,0,523,2,"FLOAT"],[1382,592,0,593,1,"VAE"],[1387,593,0,598,0,"AUDIO"],[1418,616,0,557,1,"CLIP"],[1419,616,0,536,0,"CLIP"],[1420,616,0,537,0,"CLIP"],[1423,617,0,207,0,"VAE"],[1426,524,0,621,0,"INT"],[1457,630,0,275,1,"MASK"],[1481,635,0,597,4,"FLOAT"],[1531,548,0,510,4,"LATENT"],[1546,510,1,556,0,"LATENT"],[1547,646,0,557,0,"MODEL"],[1548,646,2,214,0,"VAE"],[1566,533,0,630,1,"INT"],[1567,533,1,630,2,"INT"],[1568,527,0,652,0,"SIGMAS"],[1569,652,0,510,3,"SIGMAS"],[1602,550,0,274,0,"AUDIO"],[1608,556,1,550,0,"LATENT"],[1609,535,0,548,1,"LATENT"],[1611,596,1,593,0,"LATENT"],[1714,551,0,549,1,"AUDIO"],[1852,557,0,719,0,"MODEL"],[1853,719,0,718,0,"MODEL"],[1859,557,0,722,0,"MODEL"],[1860,722,0,723,0,"MODEL"],[1886,559,0,731,2,"VAE"],[1888,527,0,731,4,"SIGMAS"],[1889,731,0,653,0,"MODEL"],[1900,556,0,734,2,"LATENT"],[1905,734,2,552,0,"LATENT"],[1942,594,0,740,1,"VAE"],[1956,743,0,744,1,"LATENT_UPSCALE_MODEL"],[1957,589,0,744,2,"VAE"],[2017,718,0,753,0,"MODEL"],[2018,753,0,583,0,"MODEL"],[2025,740,0,755,0,"IMAGE"],[2026,755,0,597,0,"IMAGE"],[2043,653,0,510,1,"GUIDER"],[2075,559,0,767,2,"VAE"],[2088,723,0,731,0,"MODEL"],[2091,734,2,744,0,"LATENT"],[2103,531,0,769,0,"IMAGE"],[2106,531,0,331,0,"IMAGE"],[2107,714,0,770,4,"IMAGE"],[2109,589,0,770,0,"VAE"],[2110,770,0,591,0,"LATENT"],[2111,531,0,731,1,"IMAGE"],[2117,534,0,772,1,"LATENT"],[2118,559,0,772,0,"VAE"],[2120,772,0,548,0,"LATENT"],[2122,744,0,770,1,"LATENT"],[2123,772,0,767,3,"LATENT"],[2137,734,0,583,1,"CONDITIONING"],[2138,734,1,583,2,"CONDITIONING"],[2139,531,0,767,4,"IMAGE"],[2140,531,0,772,4,"IMAGE"],[2141,534,0,731,3,"LATENT"],[2142,536,0,523,0,"CONDITIONING"],[2143,537,0,523,1,"CONDITIONING"],[2144,767,0,653,1,"CONDITIONING"],[2145,767,1,653,2,"CONDITIONING"],[2146,767,0,734,0,"CONDITIONING"],[2147,767,1,734,1,"CONDITIONING"],[2148,523,0,767,0,"CONDITIONING"],[2149,523,1,767,1,"CONDITIONING"],[2150,773,0,531,0,"IMAGE"],[2172,596,0,740,0,"LATENT"],[2179,593,0,597,1,"AUDIO"],[2180,779,0,722,4,"FLOAT"],[2181,780,0,723,4,"FLOAT"],[2182,781,0,718,4,"FLOAT"],[2183,782,0,719,4,"FLOAT"],[2184,783,0,722,3,"COMBO"],[2185,783,0,723,3,"COMBO"],[2186,783,0,719,3,"COMBO"],[2187,783,0,718,3,"COMBO"],[2206,584,0,788,0,"NOISE"],[2207,583,0,788,1,"GUIDER"],[2208,585,0,788,2,"SAMPLER"],[2209,582,0,788,3,"SIGMAS"],[2210,591,0,788,4,"LATENT"],[2211,788,1,596,0,"LATENT"],[2212,274,0,789,0,"LATENT"],[2213,556,1,789,1,"LATENT"],[2214,789,0,591,1,"LATENT"],[2215,791,0,531,2,"INT"],[2216,792,0,531,3,"INT"],[2218,797,0,767,6,"FLOAT"],[2219,797,0,772,3,"FLOAT"],[2220,796,0,798,0,"INT"],[2221,798,1,538,0,"INT"]],"groups":[{"id":27,"title":"Main","bounding":[-1494.9909480082615,3161.937903314709,3804.2302523868716,2304.222673775987],"color":"#3f789e","flags":{}},{"id":23,"title":"Force First-pass Audio","bounding":[1228.7248700088794,2801.335742850906,537.4514362675345,323.87650696724893],"color":"#3f789e","flags":{}},{"id":29,"title":"Final Upscale Pass","bounding":[2374.9914506547025,3167.3647232660733,2118.8972740162553,1955.3713894900138],"color":"#3f789e","flags":{}},{"id":30,"title":"Distilation Break-up (Thanks Lightricks For This Wonderful Distilled Lora)","bounding":[-167.0518161898845,2841.0578682916353,1330.1373185243046,278.6386435094696],"color":"#3f789e","flags":{}},{"id":32,"title":"Post Options, Add interpolation here before RTX","bounding":[4693.505674922303,3438.368751942385,687.0438904788525,342.37525726057083],"color":"#3f789e","flags":{}},{"id":33,"title":"Parameters","bounding":[-1444.103685997252,3815.005816195167,642.0089842779156,461.4995675177156],"color":"#3f789e","flags":{}}],"config":{},"extra":{"ds":{"scale":0.9090909090909107,"offset":[1758.6267071402829,-3662.4662806693705]},"frontendVersion":"1.43.17","workflowRendererVersion":"LG","prompt":{"1":{"inputs":{"ckpt_name":"ltx-av-step-1751000_vocoder_24K.safetensors"},"class_type":"CheckpointLoaderSimple","_meta":{"title":"Load Checkpoint"}},"2":{"inputs":{"gemma_path":"gemma-3-12b-it-qat-q4_0-unquantized_readout_proj/model/model.safetensors","ltxv_path":"ltx-av-step-1751000_vocoder_24K.safetensors","max_length":1024},"class_type":"LTXVGemmaCLIPModelLoader","_meta":{"title":"🅛🅣🅧 Gemma 3 Model Loader"}},"3":{"inputs":{"text":"A medium close-up shot features a Caucasian man with a closely shaven head and face, wearing a black baseball cap with \"PNTR\" in white letters on the front, and a dark grey t-shirt with \"JUST DO IT\" visible across his chest. A small black microphone is clipped to his shirt collar. He is positioned slightly to the left of the frame, looking intently downwards and to his right, his eyes focused off-camera. His facial expression is one of deep concentration, with his brow slightly furrowed. As he looks down, a quick sniff sound is heard, and then he speaks with a deep male voice and a slightly frustrated tone, saying, \"I think it's so bad.\" The camera remains static throughout, maintaining a shallow depth of field, which keeps the man in sharp focus while the background is softly blurred, revealing a light-colored wall with white wooden shelving or trim, and a partially open white wooden door on the right. After a brief pause, another short, audible sniff is heard. The man then continues to speak, his voice maintaining the same quality, as he states, \"So bad. So bad.\" He elaborates further, emphasizing his point with a final statement, \"This got to be, it's got to be the worst tool I've ever seen.\"","clip":["2",0]},"class_type":"CLIPTextEncode","_meta":{"title":"CLIP Text Encode (Prompt)"}},"4":{"inputs":{"text":"blurry, out of focus, overexposed, underexposed, low contrast, washed out colors, excessive noise, grainy texture, poor lighting, flickering, motion blur, distorted proportions, unnatural skin tones, deformed facial features, asymmetrical face, missing facial features, extra limbs, disfigured hands, wrong hand count, artifacts around text, unreadable text on shirt or hat, incorrect lettering on cap (“PNTR”), incorrect t-shirt slogan (“JUST DO IT”), missing microphone, misplaced microphone, inconsistent perspective, camera shake, incorrect depth of field, background too sharp, background clutter, distracting reflections, harsh shadows, inconsistent lighting direction, color banding, cartoonish rendering, 3D CGI look, unrealistic materials, uncanny valley effect, incorrect ethnicity, wrong gender, exaggerated expressions, smiling, laughing, exaggerated sadness, wrong gaze direction, eyes looking at camera, mismatched lip sync, silent or muted audio, distorted voice, robotic voice, echo, background noise, off-sync audio, missing sniff sounds, incorrect dialogue, added dialogue, repetitive speech, jittery movement, awkward pauses, incorrect timing, unnatural transitions, inconsistent framing, tilted camera, missing door or shelves, missing shallow depth of field, flat lighting, inconsistent tone, cinematic oversaturation, stylized filters, or AI artifacts.","clip":["2",0]},"class_type":"CLIPTextEncode","_meta":{"title":"CLIP Text Encode (Prompt)"}},"8":{"inputs":{"sampler_name":"euler"},"class_type":"KSamplerSelect","_meta":{"title":"KSamplerSelect"}},"9":{"inputs":{"steps":20,"max_shift":2.05,"base_shift":0.95,"stretch":true,"terminal":0.1,"latent":["28",0]},"class_type":"LTXVScheduler","_meta":{"title":"LTXVScheduler"}},"11":{"inputs":{"noise_seed":10},"class_type":"RandomNoise","_meta":{"title":"RandomNoise"}},"12":{"inputs":{"samples":["29",0],"vae":["1",2]},"class_type":"VAEDecode","_meta":{"title":"VAE Decode"}},"13":{"inputs":{"ckpt_name":"ltx-av-step-1751000_vocoder_24K.safetensors"},"class_type":"LTXVAudioVAELoader","_meta":{"title":"🅛🅣🅧 LTXV Audio VAE Loader"}},"14":{"inputs":{"samples":["29",1],"audio_vae":["13",0]},"class_type":"LTXVAudioVAEDecode","_meta":{"title":"🅛🅣🅧 LTXV Audio VAE Decode"}},"15":{"inputs":{"frame_rate":["23",0],"loop_count":0,"filename_prefix":"AnimateDiff","format":"video/h264-mp4","pix_fmt":"yuv420p","crf":19,"save_metadata":true,"trim_to_audio":false,"pingpong":false,"save_output":true,"images":["12",0],"audio":["14",0]},"class_type":"VHS_VideoCombine","_meta":{"title":"Video Combine 🎥🅥🅗🅢"}},"17":{"inputs":{"skip_blocks":"29","model":["28",1],"positive":["22",0],"negative":["22",1],"parameters":["18",0]},"class_type":"MultimodalGuider","_meta":{"title":"🅛🅣🅧 Multimodal Guider"}},"18":{"inputs":{"modality":"VIDEO","cfg":3,"stg":0,"rescale":0,"modality_scale":3,"parameters":["19",0]},"class_type":"GuiderParameters","_meta":{"title":"🅛🅣🅧 Guider Parameters"}},"19":{"inputs":{"modality":"AUDIO","cfg":7,"stg":0,"rescale":0,"modality_scale":3},"class_type":"GuiderParameters","_meta":{"title":"🅛🅣🅧 Guider Parameters"}},"21":{"inputs":{"audioUI":"","audio":["14",0]},"class_type":"PreviewAudio","_meta":{"title":"PreviewAudio"}},"22":{"inputs":{"frame_rate":["23",0],"positive":["3",0],"negative":["4",0]},"class_type":"LTXVConditioning","_meta":{"title":"LTXVConditioning"}},"23":{"inputs":{"value":25},"class_type":"FloatConstant","_meta":{"title":"Float Constant"}},"26":{"inputs":{"frames_number":["27",0],"frame_rate":["42",0],"batch_size":1},"class_type":"LTXVEmptyLatentAudio","_meta":{"title":"🅛🅣🅧 LTXV Empty Latent Audio"}},"27":{"inputs":{"value":105},"class_type":"INTConstant","_meta":{"title":"INT Constant"}},"28":{"inputs":{"video_latent":["43",0],"audio_latent":["26",0],"model":["44",0]},"class_type":"LTXVConcatAVLatent","_meta":{"title":"🅛🅣🅧 LTXV Concat AV Latent"}},"29":{"inputs":{"av_latent":["41",0],"model":["28",1]},"class_type":"LTXVSeparateAVLatent","_meta":{"title":"🅛🅣🅧 LTXV Separate AV Latent"}},"41":{"inputs":{"noise":["11",0],"guider":["17",0],"sampler":["8",0],"sigmas":["9",0],"latent_image":["28",0]},"class_type":"SamplerCustomAdvanced","_meta":{"title":"SamplerCustomAdvanced"}},"42":{"inputs":{"a":["23",0]},"class_type":"CM_FloatToInt","_meta":{"title":"FloatToInt"}},"43":{"inputs":{"width":768,"height":512,"length":["27",0],"batch_size":1},"class_type":"EmptyLTXVLatentVideo","_meta":{"title":"EmptyLTXVLatentVideo"}},"44":{"inputs":{"torch_compile":true,"disable_backup":false,"model":["1",0]},"class_type":"LTXVSequenceParallelMultiGPUPatcher","_meta":{"title":"LTXVSequenceParallelMultiGPUPatcher"}},"45":{"inputs":{"frame_idx":0,"strength":1},"class_type":"LTXVAddGuide","_meta":{"title":"LTXVAddGuide"}}},"comfy_fork_version":"feature/av_inference@a6994ed1","VHS_latentpreview":false,"VHS_latentpreviewrate":0,"VHS_MetadataImage":true,"VHS_KeepIntermediate":true,"ue_links":[],"links_added_by_ue":[]},"version":0.4}
custom_nodes/10S-Comfy-nodes/workflows/10Eros_10SNodes_LikenessGuideHelper_I2V_v3.2.json ADDED
The diff for this file is too large to render. See raw diff
 
custom_nodes/ComfyUI-GGUF/tools/README.md ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## Converting initial model
2
+
3
+ To convert your initial safetensors/ckpt model to FP16/BF16 GGUF, run the following command:
4
+
5
+ ```
6
+ python convert.py --src E:\models\unet\flux1-dev.safetensors
7
+ ```
8
+ Make sure `gguf>=0.13.0` is installed for this step. Optionally, specify the output gguf file with the `--dst` arg.
9
+
10
+ > [!NOTE]
11
+ > Do not use the diffusers UNET format for flux, it won't work, use the default/reference checkpoint key format. This is due to q/k/v being merged into one qkv key.
12
+ > You can convert it by loading it in ComfyUI and saving it using the built-in "ModelSave" node.
13
+
14
+ > [!WARNING]
15
+ > For hunyuan video/wan 2.1, you will see a warning about 5D tensors. This means the script will save a **non functional** model to disk first, that you can quantize. I recommend saving these in a separate `raw` folder to avoid confusion.
16
+ >
17
+ > After quantization, you will have to run `fix_5d_tensor.py` manually to add back the missing key that was saved by the conversion code.
18
+
19
+ ## Quantizing using custom llama.cpp
20
+
21
+ Depending on your git settings, you may need to run the following script first in order to make sure the patch file is valid. It will convert Windows (CRLF) line endings to Unix (LF) ones.
22
+
23
+ ```
24
+ python fix_lines_ending.py
25
+ ```
26
+
27
+ Git clone llama.cpp into the current folder:
28
+
29
+ ```
30
+ git clone https://github.com/ggerganov/llama.cpp
31
+ ```
32
+
33
+ Check out the correct branch, then apply the custom patch needed to add image model support to the repo you just cloned.
34
+
35
+ ```
36
+ cd llama.cpp
37
+ git checkout tags/b3962
38
+ git apply ..\lcpp.patch
39
+ ```
40
+
41
+ Compile the llama-quantize binary. This example uses cmake, on linux you can just use make.
42
+
43
+ ### Visual Studio 2019, Linux, etc...
44
+
45
+ ```
46
+ mkdir build
47
+ cmake -B build
48
+ cmake --build build --config Debug -j10 --target llama-quantize
49
+ cd ..
50
+ ```
51
+
52
+ ### Visual Studio 2022
53
+
54
+ ```
55
+ mkdir build
56
+ cmake -B build -DCMAKE_CXX_STANDARD=17 -DCMAKE_CXX_STANDARD_REQUIRED=ON -DCMAKE_CXX_FLAGS="-std=c++17"
57
+ ```
58
+
59
+ Edit the `llama.cpp\common\log.cpp` file, inserts two lines after the existing first line:
60
+
61
+ ```
62
+ #include "log.h"
63
+
64
+ #define _SILENCE_CXX23_CHRONO_DEPRECATION_WARNING
65
+ #include <chrono>
66
+ ```
67
+
68
+ Then you can build the project:
69
+ ```
70
+ cmake --build build --config Debug -j10 --target llama-quantize
71
+ cd ..
72
+ ```
73
+
74
+ ### Quantize your model
75
+
76
+
77
+ Now you can use the newly build binary to quantize your model to the desired format:
78
+ ```
79
+ llama.cpp\build\bin\Debug\llama-quantize.exe E:\models\unet\flux1-dev-BF16.gguf E:\models\unet\flux1-dev-Q4_K_S.gguf Q4_K_S
80
+ ```
81
+
82
+ You can extract the patch again with `git diff src\llama.cpp > lcpp.patch` if you wish to change something and contribute back.
83
+
84
+ > [!WARNING]
85
+ > For hunyuan video/wan 2.1, you will have to run `fix_5d_tensor.py` after the quantization step is done.
86
+ >
87
+ > Example usage: `fix_5d_tensors.py --src E:\models\video\raw\wan2.1-t2v-1.3b-Q8_0.gguf --dst E:\models\video\wan2.1-t2v-1.3b-Q8_0.gguf`
88
+ >
89
+ > By default, this also saves a `fix_5d_tensors_[arch].safetensors` file in the `ComfyUI-GGUF/tools` folder, it's recommended to delete this after all models have been converted.
90
+
91
+ > [!NOTE]
92
+ > Do not quantize SDXL / SD1 / other Conv2D heavy models. If you do, make sure to **extract the UNET model first**.
93
+ >This should be obvious, but also don't use the resulting llama-quantize binary with LLMs.
custom_nodes/ComfyUI-GGUF/tools/convert.py ADDED
@@ -0,0 +1,365 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # (c) City96 || Apache-2.0 (apache.org/licenses/LICENSE-2.0)
2
+ import os
3
+ import gguf
4
+ import torch
5
+ import logging
6
+ import argparse
7
+ from tqdm import tqdm
8
+ from safetensors.torch import load_file, save_file
9
+
10
+ QUANTIZATION_THRESHOLD = 1024
11
+ REARRANGE_THRESHOLD = 512
12
+ MAX_TENSOR_NAME_LENGTH = 127
13
+ MAX_TENSOR_DIMS = 4
14
+
15
+ class ModelTemplate:
16
+ arch = "invalid" # string describing architecture
17
+ shape_fix = False # whether to reshape tensors
18
+ keys_detect = [] # list of lists to match in state dict
19
+ keys_banned = [] # list of keys that should mark model as invalid for conversion
20
+ keys_hiprec = [] # list of keys that need to be kept in fp32 for some reason
21
+ keys_ignore = [] # list of strings to ignore keys by when found
22
+
23
+ def handle_nd_tensor(self, key, data):
24
+ raise NotImplementedError(f"Tensor detected that exceeds dims supported by C++ code! ({key} @ {data.shape})")
25
+
26
+ class ModelFlux(ModelTemplate):
27
+ arch = "flux"
28
+ keys_detect = [
29
+ ("transformer_blocks.0.attn.norm_added_k.weight",),
30
+ ("double_blocks.0.img_attn.proj.weight",),
31
+ ]
32
+ keys_banned = ["transformer_blocks.0.attn.norm_added_k.weight",]
33
+
34
+ class ModelSD3(ModelTemplate):
35
+ arch = "sd3"
36
+ keys_detect = [
37
+ ("transformer_blocks.0.attn.add_q_proj.weight",),
38
+ ("joint_blocks.0.x_block.attn.qkv.weight",),
39
+ ]
40
+ keys_banned = ["transformer_blocks.0.attn.add_q_proj.weight",]
41
+
42
+ class ModelAura(ModelTemplate):
43
+ arch = "aura"
44
+ keys_detect = [
45
+ ("double_layers.3.modX.1.weight",),
46
+ ("joint_transformer_blocks.3.ff_context.out_projection.weight",),
47
+ ]
48
+ keys_banned = ["joint_transformer_blocks.3.ff_context.out_projection.weight",]
49
+
50
+ class ModelHiDream(ModelTemplate):
51
+ arch = "hidream"
52
+ keys_detect = [
53
+ (
54
+ "caption_projection.0.linear.weight",
55
+ "double_stream_blocks.0.block.ff_i.shared_experts.w3.weight"
56
+ )
57
+ ]
58
+ keys_hiprec = [
59
+ # nn.parameter, can't load from BF16 ver
60
+ ".ff_i.gate.weight",
61
+ "img_emb.emb_pos"
62
+ ]
63
+
64
+ class CosmosPredict2(ModelTemplate):
65
+ arch = "cosmos"
66
+ keys_detect = [
67
+ (
68
+ "blocks.0.mlp.layer1.weight",
69
+ "blocks.0.adaln_modulation_cross_attn.1.weight",
70
+ )
71
+ ]
72
+ keys_hiprec = ["pos_embedder"]
73
+ keys_ignore = ["_extra_state", "accum_"]
74
+
75
+ class ModelHyVid(ModelTemplate):
76
+ arch = "hyvid"
77
+ keys_detect = [
78
+ (
79
+ "double_blocks.0.img_attn_proj.weight",
80
+ "txt_in.individual_token_refiner.blocks.1.self_attn_qkv.weight",
81
+ )
82
+ ]
83
+
84
+ def handle_nd_tensor(self, key, data):
85
+ # hacky but don't have any better ideas
86
+ path = f"./fix_5d_tensors_{self.arch}.safetensors" # TODO: somehow get a path here??
87
+ if os.path.isfile(path):
88
+ raise RuntimeError(f"5D tensor fix file already exists! {path}")
89
+ fsd = {key: torch.from_numpy(data)}
90
+ tqdm.write(f"5D key found in state dict! Manual fix required! - {key} {data.shape}")
91
+ save_file(fsd, path)
92
+
93
+ class ModelWan(ModelHyVid):
94
+ arch = "wan"
95
+ keys_detect = [
96
+ (
97
+ "blocks.0.self_attn.norm_q.weight",
98
+ "text_embedding.2.weight",
99
+ "head.modulation",
100
+ )
101
+ ]
102
+ keys_hiprec = [
103
+ ".modulation" # nn.parameter, can't load from BF16 ver
104
+ ]
105
+
106
+ class ModelLTXV(ModelTemplate):
107
+ arch = "ltxv"
108
+ keys_detect = [
109
+ (
110
+ "adaln_single.emb.timestep_embedder.linear_2.weight",
111
+ "transformer_blocks.27.scale_shift_table",
112
+ "caption_projection.linear_2.weight",
113
+ )
114
+ ]
115
+ keys_hiprec = [
116
+ "scale_shift_table" # nn.parameter, can't load from BF16 base quant
117
+ ]
118
+
119
+ class ModelSDXL(ModelTemplate):
120
+ arch = "sdxl"
121
+ shape_fix = True
122
+ keys_detect = [
123
+ ("down_blocks.0.downsamplers.0.conv.weight", "add_embedding.linear_1.weight",),
124
+ (
125
+ "input_blocks.3.0.op.weight", "input_blocks.6.0.op.weight",
126
+ "output_blocks.2.2.conv.weight", "output_blocks.5.2.conv.weight",
127
+ ), # Non-diffusers
128
+ ("label_emb.0.0.weight",),
129
+ ]
130
+
131
+ class ModelSD1(ModelTemplate):
132
+ arch = "sd1"
133
+ shape_fix = True
134
+ keys_detect = [
135
+ ("down_blocks.0.downsamplers.0.conv.weight",),
136
+ (
137
+ "input_blocks.3.0.op.weight", "input_blocks.6.0.op.weight", "input_blocks.9.0.op.weight",
138
+ "output_blocks.2.1.conv.weight", "output_blocks.5.2.conv.weight", "output_blocks.8.2.conv.weight"
139
+ ), # Non-diffusers
140
+ ]
141
+
142
+ class ModelLumina2(ModelTemplate):
143
+ arch = "lumina2"
144
+ keys_detect = [
145
+ ("cap_embedder.1.weight", "context_refiner.0.attention.qkv.weight")
146
+ ]
147
+
148
+ arch_list = [ModelFlux, ModelSD3, ModelAura, ModelHiDream, CosmosPredict2,
149
+ ModelLTXV, ModelHyVid, ModelWan, ModelSDXL, ModelSD1, ModelLumina2]
150
+
151
+ def is_model_arch(model, state_dict):
152
+ # check if model is correct
153
+ matched = False
154
+ invalid = False
155
+ for match_list in model.keys_detect:
156
+ if all(key in state_dict for key in match_list):
157
+ matched = True
158
+ invalid = any(key in state_dict for key in model.keys_banned)
159
+ break
160
+ assert not invalid, "Model architecture not allowed for conversion! (i.e. reference VS diffusers format)"
161
+ return matched
162
+
163
+ def detect_arch(state_dict):
164
+ model_arch = None
165
+ for arch in arch_list:
166
+ if is_model_arch(arch, state_dict):
167
+ model_arch = arch()
168
+ break
169
+ assert model_arch is not None, "Unknown model architecture!"
170
+ return model_arch
171
+
172
+ def parse_args():
173
+ parser = argparse.ArgumentParser(description="Generate F16 GGUF files from single UNET")
174
+ parser.add_argument("--src", required=True, help="Source model ckpt file.")
175
+ parser.add_argument("--dst", help="Output unet gguf file.")
176
+ args = parser.parse_args()
177
+
178
+ if not os.path.isfile(args.src):
179
+ parser.error("No input provided!")
180
+
181
+ return args
182
+
183
+ def strip_prefix(state_dict):
184
+ # prefix for mixed state dict
185
+ prefix = None
186
+ for pfx in ["model.diffusion_model.", "model."]:
187
+ if any([x.startswith(pfx) for x in state_dict.keys()]):
188
+ prefix = pfx
189
+ break
190
+
191
+ # prefix for uniform state dict
192
+ if prefix is None:
193
+ for pfx in ["net."]:
194
+ if all([x.startswith(pfx) for x in state_dict.keys()]):
195
+ prefix = pfx
196
+ break
197
+
198
+ # strip prefix if found
199
+ if prefix is not None:
200
+ logging.info(f"State dict prefix found: '{prefix}'")
201
+ sd = {}
202
+ for k, v in state_dict.items():
203
+ if prefix not in k:
204
+ continue
205
+ k = k.replace(prefix, "")
206
+ sd[k] = v
207
+ else:
208
+ logging.debug("State dict has no prefix")
209
+ sd = state_dict
210
+
211
+ return sd
212
+
213
+ def load_state_dict(path):
214
+ if any(path.endswith(x) for x in [".ckpt", ".pt", ".bin", ".pth"]):
215
+ state_dict = torch.load(path, map_location="cpu", weights_only=True)
216
+ for subkey in ["model", "module"]:
217
+ if subkey in state_dict:
218
+ state_dict = state_dict[subkey]
219
+ break
220
+ if len(state_dict) < 20:
221
+ raise RuntimeError(f"pt subkey load failed: {state_dict.keys()}")
222
+ else:
223
+ state_dict = load_file(path)
224
+
225
+ return strip_prefix(state_dict)
226
+
227
+ def handle_tensors(writer, state_dict, model_arch):
228
+ name_lengths = tuple(sorted(
229
+ ((key, len(key)) for key in state_dict.keys()),
230
+ key=lambda item: item[1],
231
+ reverse=True,
232
+ ))
233
+ if not name_lengths:
234
+ return
235
+ max_name_len = name_lengths[0][1]
236
+ if max_name_len > MAX_TENSOR_NAME_LENGTH:
237
+ bad_list = ", ".join(f"{key!r} ({namelen})" for key, namelen in name_lengths if namelen > MAX_TENSOR_NAME_LENGTH)
238
+ raise ValueError(f"Can only handle tensor names up to {MAX_TENSOR_NAME_LENGTH} characters. Tensors exceeding the limit: {bad_list}")
239
+ for key, data in tqdm(state_dict.items()):
240
+ old_dtype = data.dtype
241
+
242
+ if any(x in key for x in model_arch.keys_ignore):
243
+ tqdm.write(f"Filtering ignored key: '{key}'")
244
+ continue
245
+
246
+ if data.dtype == torch.bfloat16:
247
+ data = data.to(torch.float32).numpy()
248
+ # this is so we don't break torch 2.0.X
249
+ elif data.dtype in [getattr(torch, "float8_e4m3fn", "_invalid"), getattr(torch, "float8_e5m2", "_invalid")]:
250
+ data = data.to(torch.float16).numpy()
251
+ else:
252
+ data = data.numpy()
253
+
254
+ n_dims = len(data.shape)
255
+ data_shape = data.shape
256
+ if old_dtype == torch.bfloat16:
257
+ data_qtype = gguf.GGMLQuantizationType.BF16
258
+ # elif old_dtype == torch.float32:
259
+ # data_qtype = gguf.GGMLQuantizationType.F32
260
+ else:
261
+ data_qtype = gguf.GGMLQuantizationType.F16
262
+
263
+ # The max no. of dimensions that can be handled by the quantization code is 4
264
+ if len(data.shape) > MAX_TENSOR_DIMS:
265
+ model_arch.handle_nd_tensor(key, data)
266
+ continue # needs to be added back later
267
+
268
+ # get number of parameters (AKA elements) in this tensor
269
+ n_params = 1
270
+ for dim_size in data_shape:
271
+ n_params *= dim_size
272
+
273
+ if old_dtype in (torch.float32, torch.bfloat16):
274
+ if n_dims == 1:
275
+ # one-dimensional tensors should be kept in F32
276
+ # also speeds up inference due to not dequantizing
277
+ data_qtype = gguf.GGMLQuantizationType.F32
278
+
279
+ elif n_params <= QUANTIZATION_THRESHOLD:
280
+ # very small tensors
281
+ data_qtype = gguf.GGMLQuantizationType.F32
282
+
283
+ elif any(x in key for x in model_arch.keys_hiprec):
284
+ # tensors that require max precision
285
+ data_qtype = gguf.GGMLQuantizationType.F32
286
+
287
+ if (model_arch.shape_fix # NEVER reshape for models such as flux
288
+ and n_dims > 1 # Skip one-dimensional tensors
289
+ and n_params >= REARRANGE_THRESHOLD # Only rearrange tensors meeting the size requirement
290
+ and (n_params / 256).is_integer() # Rearranging only makes sense if total elements is divisible by 256
291
+ and not (data.shape[-1] / 256).is_integer() # Only need to rearrange if the last dimension is not divisible by 256
292
+ ):
293
+ orig_shape = data.shape
294
+ data = data.reshape(n_params // 256, 256)
295
+ writer.add_array(f"comfy.gguf.orig_shape.{key}", tuple(int(dim) for dim in orig_shape))
296
+
297
+ try:
298
+ data = gguf.quants.quantize(data, data_qtype)
299
+ except (AttributeError, gguf.QuantError) as e:
300
+ tqdm.write(f"falling back to F16: {e}")
301
+ data_qtype = gguf.GGMLQuantizationType.F16
302
+ data = gguf.quants.quantize(data, data_qtype)
303
+
304
+ new_name = key # do we need to rename?
305
+
306
+ shape_str = f"{{{', '.join(str(n) for n in reversed(data.shape))}}}"
307
+ tqdm.write(f"{f'%-{max_name_len + 4}s' % f'{new_name}'} {old_dtype} --> {data_qtype.name}, shape = {shape_str}")
308
+
309
+ writer.add_tensor(new_name, data, raw_dtype=data_qtype)
310
+
311
+ def convert_file(path, dst_path=None, interact=True, overwrite=False):
312
+ # load & run model detection logic
313
+ state_dict = load_state_dict(path)
314
+ model_arch = detect_arch(state_dict)
315
+ logging.info(f"* Architecture detected from input: {model_arch.arch}")
316
+
317
+ # detect & set dtype for output file
318
+ dtypes = [x.dtype for x in state_dict.values()]
319
+ dtypes = {x:dtypes.count(x) for x in set(dtypes)}
320
+ main_dtype = max(dtypes, key=dtypes.get)
321
+
322
+ if main_dtype == torch.bfloat16:
323
+ ftype_name = "BF16"
324
+ ftype_gguf = gguf.LlamaFileType.MOSTLY_BF16
325
+ # elif main_dtype == torch.float32:
326
+ # ftype_name = "F32"
327
+ # ftype_gguf = None
328
+ else:
329
+ ftype_name = "F16"
330
+ ftype_gguf = gguf.LlamaFileType.MOSTLY_F16
331
+
332
+ if dst_path is None:
333
+ dst_path = f"{os.path.splitext(path)[0]}-{ftype_name}.gguf"
334
+ elif "{ftype}" in dst_path: # lcpp logic
335
+ dst_path = dst_path.replace("{ftype}", ftype_name)
336
+
337
+ if os.path.isfile(dst_path) and not overwrite:
338
+ if interact:
339
+ input("Output exists enter to continue or ctrl+c to abort!")
340
+ else:
341
+ raise OSError("Output exists and overwriting is disabled!")
342
+
343
+ # handle actual file
344
+ writer = gguf.GGUFWriter(path=None, arch=model_arch.arch)
345
+ writer.add_quantization_version(gguf.GGML_QUANT_VERSION)
346
+ if ftype_gguf is not None:
347
+ writer.add_file_type(ftype_gguf)
348
+
349
+ handle_tensors(writer, state_dict, model_arch)
350
+ writer.write_header_to_file(path=dst_path)
351
+ writer.write_kv_data_to_file()
352
+ writer.write_tensors_to_file(progress=True)
353
+ writer.close()
354
+
355
+ fix = f"./fix_5d_tensors_{model_arch.arch}.safetensors"
356
+ if os.path.isfile(fix):
357
+ logging.warning(f"\n### Warning! Fix file found at '{fix}'")
358
+ logging.warning(" you most likely need to run 'fix_5d_tensors.py' after quantization.")
359
+
360
+ return dst_path, model_arch
361
+
362
+ if __name__ == "__main__":
363
+ args = parse_args()
364
+ convert_file(args.src, args.dst)
365
+
custom_nodes/ComfyUI-GGUF/tools/fix_5d_tensors.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # (c) City96 || Apache-2.0 (apache.org/licenses/LICENSE-2.0)
2
+ import os
3
+ import gguf
4
+ import torch
5
+ import argparse
6
+ from tqdm import tqdm
7
+ from safetensors.torch import load_file
8
+
9
+ def get_args():
10
+ parser = argparse.ArgumentParser()
11
+ parser.add_argument("--src", required=True)
12
+ parser.add_argument("--dst", required=True)
13
+ parser.add_argument("--fix", required=False, help="Defaults to ./fix_5d_tensors_[arch].pt")
14
+ parser.add_argument("--overwrite", action="store_true")
15
+ args = parser.parse_args()
16
+
17
+ if not os.path.isfile(args.src):
18
+ parser.error(f"Invalid source file '{args.src}'")
19
+ if not args.overwrite and os.path.exists(args.dst):
20
+ parser.error(f"Output exists, use '--overwrite' ({args.dst})")
21
+
22
+ return args
23
+
24
+ def get_arch_str(reader):
25
+ field = reader.get_field("general.architecture")
26
+ return str(field.parts[field.data[-1]], encoding="utf-8")
27
+
28
+ def get_file_type(reader):
29
+ field = reader.get_field("general.file_type")
30
+ ft = int(field.parts[field.data[-1]])
31
+ return gguf.LlamaFileType(ft)
32
+
33
+ if __name__ == "__main__":
34
+ args = get_args()
35
+
36
+ # read existing
37
+ reader = gguf.GGUFReader(args.src)
38
+ arch = get_arch_str(reader)
39
+ file_type = get_file_type(reader)
40
+ print(f"Detected arch: '{arch}' (ftype: {str(file_type)})")
41
+
42
+ # prep fix
43
+ if args.fix is None:
44
+ args.fix = f"./fix_5d_tensors_{arch}.safetensors"
45
+
46
+ if not os.path.isfile(args.fix):
47
+ raise OSError(f"No 5D tensor fix file: {args.fix}")
48
+
49
+ sd5d = load_file(args.fix)
50
+ sd5d = {k:v.numpy() for k,v in sd5d.items()}
51
+ print("5D tensors:", sd5d.keys())
52
+
53
+ # prep output
54
+ writer = gguf.GGUFWriter(path=None, arch=arch)
55
+ writer.add_quantization_version(gguf.GGML_QUANT_VERSION)
56
+ writer.add_file_type(file_type)
57
+
58
+ added = []
59
+ def add_extra_key(writer, key, data):
60
+ global added
61
+ data_qtype = gguf.GGMLQuantizationType.F32
62
+ data = gguf.quants.quantize(data, data_qtype)
63
+ tqdm.write(f"Adding key {key} ({data.shape})")
64
+ writer.add_tensor(key, data, raw_dtype=data_qtype)
65
+ added.append(key)
66
+
67
+ # main loop to add missing 5D tensor(s)
68
+ for tensor in tqdm(reader.tensors):
69
+ writer.add_tensor(tensor.name, tensor.data, raw_dtype=tensor.tensor_type)
70
+ key5d = tensor.name.replace(".bias", ".weight")
71
+ if key5d in sd5d.keys():
72
+ add_extra_key(writer, key5d, sd5d[key5d])
73
+
74
+ # brute force for any missed
75
+ for key, data in sd5d.items():
76
+ if key not in added:
77
+ add_extra_key(writer, key, data)
78
+
79
+ writer.write_header_to_file(path=args.dst)
80
+ writer.write_kv_data_to_file()
81
+ writer.write_tensors_to_file(progress=True)
82
+ writer.close()
custom_nodes/ComfyUI-GGUF/tools/fix_lines_ending.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ files = ["lcpp.patch", "lcpp_sd3.patch"]
4
+
5
+ def has_unix_line_endings(file_path):
6
+ try:
7
+ with open(file_path, 'rb') as file:
8
+ content = file.read()
9
+ return b'\r\n' not in content
10
+ except Exception as e:
11
+ print(f"Error checking '{file_path}': {e}")
12
+ return False
13
+
14
+ def convert_to_linux_format(file_path):
15
+ try:
16
+ with open(file_path, 'rb') as file:
17
+ content = file.read().replace(b'\r\n', b'\n')
18
+ with open(file_path, 'wb') as file:
19
+ file.write(content)
20
+ print(f"'{file_path}' converted to Linux line endings (LF).")
21
+ except Exception as e:
22
+ print(f"Error processing '{file_path}': {e}")
23
+
24
+ for file in files:
25
+ if os.path.exists(file):
26
+ if has_unix_line_endings(file):
27
+ print(f"'{file}' already has Unix line endings (LF). No conversion needed.")
28
+ else:
29
+ convert_to_linux_format(file)
30
+ else:
31
+ print(f"File '{file}' does not exist.")
custom_nodes/ComfyUI-GGUF/tools/lcpp.patch ADDED
@@ -0,0 +1,451 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ diff --git a/ggml/include/ggml.h b/ggml/include/ggml.h
2
+ index de3c706f..0267c1fa 100644
3
+ --- a/ggml/include/ggml.h
4
+ +++ b/ggml/include/ggml.h
5
+ @@ -223,7 +223,7 @@
6
+ #define GGML_MAX_OP_PARAMS 64
7
+
8
+ #ifndef GGML_MAX_NAME
9
+ -# define GGML_MAX_NAME 64
10
+ +# define GGML_MAX_NAME 128
11
+ #endif
12
+
13
+ #define GGML_DEFAULT_N_THREADS 4
14
+ @@ -2449,6 +2449,7 @@ extern "C" {
15
+
16
+ // manage tensor info
17
+ GGML_API void gguf_add_tensor(struct gguf_context * ctx, const struct ggml_tensor * tensor);
18
+ + GGML_API void gguf_set_tensor_ndim(struct gguf_context * ctx, const char * name, int n_dim);
19
+ GGML_API void gguf_set_tensor_type(struct gguf_context * ctx, const char * name, enum ggml_type type);
20
+ GGML_API void gguf_set_tensor_data(struct gguf_context * ctx, const char * name, const void * data, size_t size);
21
+
22
+ diff --git a/ggml/src/ggml.c b/ggml/src/ggml.c
23
+ index b16c462f..6d1568f1 100644
24
+ --- a/ggml/src/ggml.c
25
+ +++ b/ggml/src/ggml.c
26
+ @@ -22960,6 +22960,14 @@ void gguf_add_tensor(
27
+ ctx->header.n_tensors++;
28
+ }
29
+
30
+ +void gguf_set_tensor_ndim(struct gguf_context * ctx, const char * name, const int n_dim) {
31
+ + const int idx = gguf_find_tensor(ctx, name);
32
+ + if (idx < 0) {
33
+ + GGML_ABORT("tensor not found");
34
+ + }
35
+ + ctx->infos[idx].n_dims = n_dim;
36
+ +}
37
+ +
38
+ void gguf_set_tensor_type(struct gguf_context * ctx, const char * name, enum ggml_type type) {
39
+ const int idx = gguf_find_tensor(ctx, name);
40
+ if (idx < 0) {
41
+ diff --git a/src/llama.cpp b/src/llama.cpp
42
+ index 24e1f1f0..25db4c69 100644
43
+ --- a/src/llama.cpp
44
+ +++ b/src/llama.cpp
45
+ @@ -205,6 +205,17 @@ enum llm_arch {
46
+ LLM_ARCH_GRANITE,
47
+ LLM_ARCH_GRANITE_MOE,
48
+ LLM_ARCH_CHAMELEON,
49
+ + LLM_ARCH_FLUX,
50
+ + LLM_ARCH_SD1,
51
+ + LLM_ARCH_SDXL,
52
+ + LLM_ARCH_SD3,
53
+ + LLM_ARCH_AURA,
54
+ + LLM_ARCH_LTXV,
55
+ + LLM_ARCH_HYVID,
56
+ + LLM_ARCH_WAN,
57
+ + LLM_ARCH_HIDREAM,
58
+ + LLM_ARCH_COSMOS,
59
+ + LLM_ARCH_LUMINA2,
60
+ LLM_ARCH_UNKNOWN,
61
+ };
62
+
63
+ @@ -258,6 +269,17 @@ static const std::map<llm_arch, const char *> LLM_ARCH_NAMES = {
64
+ { LLM_ARCH_GRANITE, "granite" },
65
+ { LLM_ARCH_GRANITE_MOE, "granitemoe" },
66
+ { LLM_ARCH_CHAMELEON, "chameleon" },
67
+ + { LLM_ARCH_FLUX, "flux" },
68
+ + { LLM_ARCH_SD1, "sd1" },
69
+ + { LLM_ARCH_SDXL, "sdxl" },
70
+ + { LLM_ARCH_SD3, "sd3" },
71
+ + { LLM_ARCH_AURA, "aura" },
72
+ + { LLM_ARCH_LTXV, "ltxv" },
73
+ + { LLM_ARCH_HYVID, "hyvid" },
74
+ + { LLM_ARCH_WAN, "wan" },
75
+ + { LLM_ARCH_HIDREAM, "hidream" },
76
+ + { LLM_ARCH_COSMOS, "cosmos" },
77
+ + { LLM_ARCH_LUMINA2, "lumina2" },
78
+ { LLM_ARCH_UNKNOWN, "(unknown)" },
79
+ };
80
+
81
+ @@ -1531,6 +1553,17 @@ static const std::map<llm_arch, std::map<llm_tensor, const char *>> LLM_TENSOR_N
82
+ { LLM_TENSOR_ATTN_K_NORM, "blk.%d.attn_k_norm" },
83
+ },
84
+ },
85
+ + { LLM_ARCH_FLUX, {}},
86
+ + { LLM_ARCH_SD1, {}},
87
+ + { LLM_ARCH_SDXL, {}},
88
+ + { LLM_ARCH_SD3, {}},
89
+ + { LLM_ARCH_AURA, {}},
90
+ + { LLM_ARCH_LTXV, {}},
91
+ + { LLM_ARCH_HYVID, {}},
92
+ + { LLM_ARCH_WAN, {}},
93
+ + { LLM_ARCH_HIDREAM, {}},
94
+ + { LLM_ARCH_COSMOS, {}},
95
+ + { LLM_ARCH_LUMINA2, {}},
96
+ {
97
+ LLM_ARCH_UNKNOWN,
98
+ {
99
+ @@ -5403,6 +5436,25 @@ static void llm_load_hparams(
100
+ // get general kv
101
+ ml.get_key(LLM_KV_GENERAL_NAME, model.name, false);
102
+
103
+ + // Disable LLM metadata for image models
104
+ + switch (model.arch) {
105
+ + case LLM_ARCH_FLUX:
106
+ + case LLM_ARCH_SD1:
107
+ + case LLM_ARCH_SDXL:
108
+ + case LLM_ARCH_SD3:
109
+ + case LLM_ARCH_AURA:
110
+ + case LLM_ARCH_LTXV:
111
+ + case LLM_ARCH_HYVID:
112
+ + case LLM_ARCH_WAN:
113
+ + case LLM_ARCH_HIDREAM:
114
+ + case LLM_ARCH_COSMOS:
115
+ + case LLM_ARCH_LUMINA2:
116
+ + model.ftype = ml.ftype;
117
+ + return;
118
+ + default:
119
+ + break;
120
+ + }
121
+ +
122
+ // get hparams kv
123
+ ml.get_key(LLM_KV_VOCAB_SIZE, hparams.n_vocab, false) || ml.get_arr_n(LLM_KV_TOKENIZER_LIST, hparams.n_vocab);
124
+
125
+ @@ -18016,6 +18068,134 @@ static void llama_tensor_dequantize_internal(
126
+ workers.clear();
127
+ }
128
+
129
+ +static ggml_type img_tensor_get_type(quantize_state_internal & qs, ggml_type new_type, const ggml_tensor * tensor, llama_ftype ftype) {
130
+ + // Special function for quantizing image model tensors
131
+ + const std::string name = ggml_get_name(tensor);
132
+ + const llm_arch arch = qs.model.arch;
133
+ +
134
+ + // Sanity check
135
+ + if (
136
+ + (name.find("model.diffusion_model.") != std::string::npos) ||
137
+ + (name.find("first_stage_model.") != std::string::npos) ||
138
+ + (name.find("single_transformer_blocks.") != std::string::npos) ||
139
+ + (name.find("joint_transformer_blocks.") != std::string::npos)
140
+ + ) {
141
+ + throw std::runtime_error("Invalid input GGUF file. This is not a supported UNET model");
142
+ + }
143
+ +
144
+ + // Unsupported quant types - exclude all IQ quants for now
145
+ + if (ftype == LLAMA_FTYPE_MOSTLY_IQ2_XXS || ftype == LLAMA_FTYPE_MOSTLY_IQ2_XS ||
146
+ + ftype == LLAMA_FTYPE_MOSTLY_IQ2_S || ftype == LLAMA_FTYPE_MOSTLY_IQ2_M ||
147
+ + ftype == LLAMA_FTYPE_MOSTLY_IQ3_XXS || ftype == LLAMA_FTYPE_MOSTLY_IQ1_S ||
148
+ + ftype == LLAMA_FTYPE_MOSTLY_IQ1_M || ftype == LLAMA_FTYPE_MOSTLY_IQ4_NL ||
149
+ + ftype == LLAMA_FTYPE_MOSTLY_IQ4_XS || ftype == LLAMA_FTYPE_MOSTLY_IQ3_S ||
150
+ + ftype == LLAMA_FTYPE_MOSTLY_IQ3_M || ftype == LLAMA_FTYPE_MOSTLY_Q4_0_4_4 ||
151
+ + ftype == LLAMA_FTYPE_MOSTLY_Q4_0_4_8 || ftype == LLAMA_FTYPE_MOSTLY_Q4_0_8_8) {
152
+ + throw std::runtime_error("Invalid quantization type for image model (Not supported)");
153
+ + }
154
+ +
155
+ + if ( // Rules for to_v attention
156
+ + (name.find("attn_v.weight") != std::string::npos) ||
157
+ + (name.find(".to_v.weight") != std::string::npos) ||
158
+ + (name.find(".v.weight") != std::string::npos) ||
159
+ + (name.find(".attn.w1v.weight") != std::string::npos) ||
160
+ + (name.find(".attn.w2v.weight") != std::string::npos) ||
161
+ + (name.find("_attn.v_proj.weight") != std::string::npos)
162
+ + ){
163
+ + if (ftype == LLAMA_FTYPE_MOSTLY_Q2_K) {
164
+ + new_type = GGML_TYPE_Q3_K;
165
+ + }
166
+ + else if (ftype == LLAMA_FTYPE_MOSTLY_Q3_K_M) {
167
+ + new_type = qs.i_attention_wv < 2 ? GGML_TYPE_Q5_K : GGML_TYPE_Q4_K;
168
+ + }
169
+ + else if (ftype == LLAMA_FTYPE_MOSTLY_Q3_K_L) {
170
+ + new_type = GGML_TYPE_Q5_K;
171
+ + }
172
+ + else if (ftype == LLAMA_FTYPE_MOSTLY_Q4_K_M || ftype == LLAMA_FTYPE_MOSTLY_Q5_K_M) {
173
+ + new_type = GGML_TYPE_Q6_K;
174
+ + }
175
+ + else if (ftype == LLAMA_FTYPE_MOSTLY_Q4_K_S && qs.i_attention_wv < 4) {
176
+ + new_type = GGML_TYPE_Q5_K;
177
+ + }
178
+ + ++qs.i_attention_wv;
179
+ + } else if ( // Rules for fused qkv attention
180
+ + (name.find("attn_qkv.weight") != std::string::npos) ||
181
+ + (name.find("attn.qkv.weight") != std::string::npos) ||
182
+ + (name.find("attention.qkv.weight") != std::string::npos)
183
+ + ) {
184
+ + if (ftype == LLAMA_FTYPE_MOSTLY_Q3_K_M || ftype == LLAMA_FTYPE_MOSTLY_Q3_K_L) {
185
+ + new_type = GGML_TYPE_Q4_K;
186
+ + }
187
+ + else if (ftype == LLAMA_FTYPE_MOSTLY_Q4_K_M) {
188
+ + new_type = GGML_TYPE_Q5_K;
189
+ + }
190
+ + else if (ftype == LLAMA_FTYPE_MOSTLY_Q5_K_M) {
191
+ + new_type = GGML_TYPE_Q6_K;
192
+ + }
193
+ + } else if ( // Rules for ffn
194
+ + (name.find("ffn_down") != std::string::npos) ||
195
+ + ((name.find("experts.") != std::string::npos) && (name.find(".w2.weight") != std::string::npos)) ||
196
+ + (name.find(".ffn.2.weight") != std::string::npos) || // is this even the right way around?
197
+ + (name.find(".ff.net.2.weight") != std::string::npos) ||
198
+ + (name.find(".mlp.layer2.weight") != std::string::npos) ||
199
+ + (name.find(".adaln_modulation_mlp.2.weight") != std::string::npos) ||
200
+ + (name.find(".feed_forward.w2.weight") != std::string::npos)
201
+ + ) {
202
+ + // TODO: add back `layer_info` with some model specific logic + logic further down
203
+ + if (ftype == LLAMA_FTYPE_MOSTLY_Q3_K_M) {
204
+ + new_type = GGML_TYPE_Q4_K;
205
+ + }
206
+ + else if (ftype == LLAMA_FTYPE_MOSTLY_Q3_K_L) {
207
+ + new_type = GGML_TYPE_Q5_K;
208
+ + }
209
+ + else if (ftype == LLAMA_FTYPE_MOSTLY_Q4_K_S) {
210
+ + new_type = GGML_TYPE_Q5_K;
211
+ + }
212
+ + else if (ftype == LLAMA_FTYPE_MOSTLY_Q4_K_M) {
213
+ + new_type = GGML_TYPE_Q6_K;
214
+ + }
215
+ + else if (ftype == LLAMA_FTYPE_MOSTLY_Q5_K_M) {
216
+ + new_type = GGML_TYPE_Q6_K;
217
+ + }
218
+ + else if (ftype == LLAMA_FTYPE_MOSTLY_Q4_0) {
219
+ + new_type = GGML_TYPE_Q4_1;
220
+ + }
221
+ + else if (ftype == LLAMA_FTYPE_MOSTLY_Q5_0) {
222
+ + new_type = GGML_TYPE_Q5_1;
223
+ + }
224
+ + ++qs.i_ffn_down;
225
+ + }
226
+ +
227
+ + // Sanity check for row shape
228
+ + bool convert_incompatible_tensor = false;
229
+ + if (new_type == GGML_TYPE_Q2_K || new_type == GGML_TYPE_Q3_K || new_type == GGML_TYPE_Q4_K ||
230
+ + new_type == GGML_TYPE_Q5_K || new_type == GGML_TYPE_Q6_K) {
231
+ + int nx = tensor->ne[0];
232
+ + int ny = tensor->ne[1];
233
+ + if (nx % QK_K != 0) {
234
+ + LLAMA_LOG_WARN("\n\n%s : tensor cols %d x %d are not divisible by %d, required for %s", __func__, nx, ny, QK_K, ggml_type_name(new_type));
235
+ + convert_incompatible_tensor = true;
236
+ + } else {
237
+ + ++qs.n_k_quantized;
238
+ + }
239
+ + }
240
+ + if (convert_incompatible_tensor) {
241
+ + // TODO: Possibly reenable this in the future
242
+ + // switch (new_type) {
243
+ + // case GGML_TYPE_Q2_K:
244
+ + // case GGML_TYPE_Q3_K:
245
+ + // case GGML_TYPE_Q4_K: new_type = GGML_TYPE_Q5_0; break;
246
+ + // case GGML_TYPE_Q5_K: new_type = GGML_TYPE_Q5_1; break;
247
+ + // case GGML_TYPE_Q6_K: new_type = GGML_TYPE_Q8_0; break;
248
+ + // default: throw std::runtime_error("\nUnsupported tensor size encountered\n");
249
+ + // }
250
+ + new_type = GGML_TYPE_F16;
251
+ + LLAMA_LOG_WARN(" - using fallback quantization %s\n", ggml_type_name(new_type));
252
+ + ++qs.n_fallback;
253
+ + }
254
+ + return new_type;
255
+ +}
256
+ +
257
+ static ggml_type llama_tensor_get_type(quantize_state_internal & qs, ggml_type new_type, const ggml_tensor * tensor, llama_ftype ftype) {
258
+ const std::string name = ggml_get_name(tensor);
259
+
260
+ @@ -18513,7 +18693,9 @@ static void llama_model_quantize_internal(const std::string & fname_inp, const s
261
+ if (llama_model_has_encoder(&model)) {
262
+ n_attn_layer *= 3;
263
+ }
264
+ - GGML_ASSERT((qs.n_attention_wv == n_attn_layer) && "n_attention_wv is unexpected");
265
+ + if (model.arch != LLM_ARCH_HYVID) { // TODO: Check why this fails
266
+ + GGML_ASSERT((qs.n_attention_wv == n_attn_layer) && "n_attention_wv is unexpected");
267
+ + }
268
+ }
269
+
270
+ size_t total_size_org = 0;
271
+ @@ -18547,6 +18729,51 @@ static void llama_model_quantize_internal(const std::string & fname_inp, const s
272
+ ctx_outs[i_split] = gguf_init_empty();
273
+ }
274
+ gguf_add_tensor(ctx_outs[i_split], tensor);
275
+ + // SD3 pos_embed needs special fix as first dim is 1, which gets truncated here
276
+ + if (model.arch == LLM_ARCH_SD3) {
277
+ + const std::string name = ggml_get_name(tensor);
278
+ + if (name == "pos_embed" && tensor->ne[2] == 1) {
279
+ + const int n_dim = 3;
280
+ + gguf_set_tensor_ndim(ctx_outs[i_split], "pos_embed", n_dim);
281
+ + LLAMA_LOG_INFO("\n%s: Correcting pos_embed shape for SD3: [key:%s]\n", __func__, tensor->name);
282
+ + }
283
+ + }
284
+ + // same goes for auraflow
285
+ + if (model.arch == LLM_ARCH_AURA) {
286
+ + const std::string name = ggml_get_name(tensor);
287
+ + if (name == "positional_encoding" && tensor->ne[2] == 1) {
288
+ + const int n_dim = 3;
289
+ + gguf_set_tensor_ndim(ctx_outs[i_split], "positional_encoding", n_dim);
290
+ + LLAMA_LOG_INFO("\n%s: Correcting positional_encoding shape for AuraFlow: [key:%s]\n", __func__, tensor->name);
291
+ + }
292
+ + if (name == "register_tokens" && tensor->ne[2] == 1) {
293
+ + const int n_dim = 3;
294
+ + gguf_set_tensor_ndim(ctx_outs[i_split], "register_tokens", n_dim);
295
+ + LLAMA_LOG_INFO("\n%s: Correcting register_tokens shape for AuraFlow: [key:%s]\n", __func__, tensor->name);
296
+ + }
297
+ + }
298
+ + // conv3d fails due to max dims - unsure what to do here as we never even reach this check
299
+ + if (model.arch == LLM_ARCH_HYVID) {
300
+ + const std::string name = ggml_get_name(tensor);
301
+ + if (name == "img_in.proj.weight" && tensor->ne[5] != 1 ) {
302
+ + throw std::runtime_error("img_in.proj.weight size failed for HyVid");
303
+ + }
304
+ + }
305
+ + // All the modulation layers also have dim1, and I think conv3d fails here too but we segfaul way before that...
306
+ + if (model.arch == LLM_ARCH_WAN) {
307
+ + const std::string name = ggml_get_name(tensor);
308
+ + if (name.find(".modulation") != std::string::npos && tensor->ne[2] == 1) {
309
+ + const int n_dim = 3;
310
+ + gguf_set_tensor_ndim(ctx_outs[i_split], tensor->name, n_dim);
311
+ + LLAMA_LOG_INFO("\n%s: Correcting shape for Wan: [key:%s]\n", __func__, tensor->name);
312
+ + }
313
+ + // FLF2V model only
314
+ + if (name == "img_emb.emb_pos") {
315
+ + const int n_dim = 3;
316
+ + gguf_set_tensor_ndim(ctx_outs[i_split], tensor->name, n_dim);
317
+ + LLAMA_LOG_INFO("\n%s: Correcting shape for Wan FLF2V: [key:%s]\n", __func__, tensor->name);
318
+ + }
319
+ + }
320
+ }
321
+
322
+ // Set split info if needed
323
+ @@ -18647,6 +18874,110 @@ static void llama_model_quantize_internal(const std::string & fname_inp, const s
324
+ // do not quantize relative position bias (T5)
325
+ quantize &= name.find("attn_rel_b.weight") == std::string::npos;
326
+
327
+ + // rules for image models
328
+ + bool image_model = false;
329
+ + if (model.arch == LLM_ARCH_FLUX) {
330
+ + image_model = true;
331
+ + quantize &= name.find("txt_in.") == std::string::npos;
332
+ + quantize &= name.find("img_in.") == std::string::npos;
333
+ + quantize &= name.find("time_in.") == std::string::npos;
334
+ + quantize &= name.find("vector_in.") == std::string::npos;
335
+ + quantize &= name.find("guidance_in.") == std::string::npos;
336
+ + quantize &= name.find("final_layer.") == std::string::npos;
337
+ + }
338
+ + if (model.arch == LLM_ARCH_SD1 || model.arch == LLM_ARCH_SDXL) {
339
+ + image_model = true;
340
+ + quantize &= name.find("class_embedding.") == std::string::npos;
341
+ + quantize &= name.find("time_embedding.") == std::string::npos;
342
+ + quantize &= name.find("add_embedding.") == std::string::npos;
343
+ + quantize &= name.find("time_embed.") == std::string::npos;
344
+ + quantize &= name.find("label_emb.") == std::string::npos;
345
+ + quantize &= name.find("conv_in.") == std::string::npos;
346
+ + quantize &= name.find("conv_out.") == std::string::npos;
347
+ + quantize &= name != "input_blocks.0.0.weight";
348
+ + quantize &= name != "out.2.weight";
349
+ + }
350
+ + if (model.arch == LLM_ARCH_SD3) {
351
+ + image_model = true;
352
+ + quantize &= name.find("final_layer.") == std::string::npos;
353
+ + quantize &= name.find("time_text_embed.") == std::string::npos;
354
+ + quantize &= name.find("context_embedder.") == std::string::npos;
355
+ + quantize &= name.find("t_embedder.") == std::string::npos;
356
+ + quantize &= name.find("y_embedder.") == std::string::npos;
357
+ + quantize &= name.find("x_embedder.") == std::string::npos;
358
+ + quantize &= name != "proj_out.weight";
359
+ + quantize &= name != "pos_embed";
360
+ + }
361
+ + if (model.arch == LLM_ARCH_AURA) {
362
+ + image_model = true;
363
+ + quantize &= name.find("t_embedder.") == std::string::npos;
364
+ + quantize &= name.find("init_x_linear.") == std::string::npos;
365
+ + quantize &= name != "modF.1.weight";
366
+ + quantize &= name != "cond_seq_linear.weight";
367
+ + quantize &= name != "final_linear.weight";
368
+ + quantize &= name != "final_linear.weight";
369
+ + quantize &= name != "positional_encoding";
370
+ + quantize &= name != "register_tokens";
371
+ + }
372
+ + if (model.arch == LLM_ARCH_LTXV) {
373
+ + image_model = true;
374
+ + quantize &= name.find("adaln_single.") == std::string::npos;
375
+ + quantize &= name.find("caption_projection.") == std::string::npos;
376
+ + quantize &= name.find("patchify_proj.") == std::string::npos;
377
+ + quantize &= name.find("proj_out.") == std::string::npos;
378
+ + quantize &= name.find("scale_shift_table") == std::string::npos; // last block too
379
+ + }
380
+ + if (model.arch == LLM_ARCH_HYVID) {
381
+ + image_model = true;
382
+ + quantize &= name.find("txt_in.") == std::string::npos;
383
+ + quantize &= name.find("img_in.") == std::string::npos;
384
+ + quantize &= name.find("time_in.") == std::string::npos;
385
+ + quantize &= name.find("vector_in.") == std::string::npos;
386
+ + quantize &= name.find("guidance_in.") == std::string::npos;
387
+ + quantize &= name.find("final_layer.") == std::string::npos;
388
+ + }
389
+ + if (model.arch == LLM_ARCH_WAN) {
390
+ + image_model = true;
391
+ + quantize &= name.find("modulation.") == std::string::npos;
392
+ + quantize &= name.find("patch_embedding.") == std::string::npos;
393
+ + quantize &= name.find("text_embedding.") == std::string::npos;
394
+ + quantize &= name.find("time_projection.") == std::string::npos;
395
+ + quantize &= name.find("time_embedding.") == std::string::npos;
396
+ + quantize &= name.find("img_emb.") == std::string::npos;
397
+ + quantize &= name.find("head.") == std::string::npos;
398
+ + }
399
+ + if (model.arch == LLM_ARCH_HIDREAM) {
400
+ + image_model = true;
401
+ + quantize &= name.find("p_embedder.") == std::string::npos;
402
+ + quantize &= name.find("t_embedder.") == std::string::npos;
403
+ + quantize &= name.find("x_embedder.") == std::string::npos;
404
+ + quantize &= name.find("final_layer.") == std::string::npos;
405
+ + quantize &= name.find(".ff_i.gate.weight") == std::string::npos;
406
+ + quantize &= name.find("caption_projection.") == std::string::npos;
407
+ + }
408
+ + if (model.arch == LLM_ARCH_COSMOS) {
409
+ + image_model = true;
410
+ + quantize &= name.find("p_embedder.") == std::string::npos;
411
+ + quantize &= name.find("t_embedder.") == std::string::npos;
412
+ + quantize &= name.find("t_embedding_norm.") == std::string::npos;
413
+ + quantize &= name.find("x_embedder.") == std::string::npos;
414
+ + quantize &= name.find("pos_embedder.") == std::string::npos;
415
+ + quantize &= name.find("final_layer.") == std::string::npos;
416
+ + }
417
+ + if (model.arch == LLM_ARCH_LUMINA2) {
418
+ + image_model = true;
419
+ + quantize &= name.find("t_embedder.") == std::string::npos;
420
+ + quantize &= name.find("x_embedder.") == std::string::npos;
421
+ + quantize &= name.find("final_layer.") == std::string::npos;
422
+ + quantize &= name.find("cap_embedder.") == std::string::npos;
423
+ + quantize &= name.find("context_refiner.") == std::string::npos;
424
+ + quantize &= name.find("noise_refiner.") == std::string::npos;
425
+ + }
426
+ + // ignore 3D/4D tensors for image models as the code was never meant to handle these
427
+ + if (image_model) {
428
+ + quantize &= ggml_n_dims(tensor) == 2;
429
+ + }
430
+ +
431
+ enum ggml_type new_type;
432
+ void * new_data;
433
+ size_t new_size;
434
+ @@ -18655,6 +18986,9 @@ static void llama_model_quantize_internal(const std::string & fname_inp, const s
435
+ new_type = default_type;
436
+
437
+ // get more optimal quantization type based on the tensor shape, layer, etc.
438
+ + if (image_model) {
439
+ + new_type = img_tensor_get_type(qs, new_type, tensor, ftype);
440
+ + } else {
441
+ if (!params->pure && ggml_is_quantized(default_type)) {
442
+ new_type = llama_tensor_get_type(qs, new_type, tensor, ftype);
443
+ }
444
+ @@ -18664,6 +18998,7 @@ static void llama_model_quantize_internal(const std::string & fname_inp, const s
445
+ if (params->output_tensor_type < GGML_TYPE_COUNT && strcmp(tensor->name, "output.weight") == 0) {
446
+ new_type = params->output_tensor_type;
447
+ }
448
+ + }
449
+
450
+ // If we've decided to quantize to the same type the tensor is already
451
+ // in then there's nothing to do.
custom_nodes/ComfyUI-GGUF/tools/read_tensors.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/python3
2
+ import os
3
+ import sys
4
+ import gguf
5
+
6
+ def read_tensors(path):
7
+ reader = gguf.GGUFReader(path)
8
+ for tensor in reader.tensors:
9
+ if tensor.tensor_type == gguf.GGMLQuantizationType.F32:
10
+ continue
11
+ print(f"{str(tensor.tensor_type):32}: {tensor.name}")
12
+
13
+ try:
14
+ path = sys.argv[1]
15
+ assert os.path.isfile(path), "Invalid path"
16
+ print(f"input: {path}")
17
+ except Exception as e:
18
+ input(f"failed: {e}")
19
+ else:
20
+ read_tensors(path)
21
+ input()
custom_nodes/ComfyUI-HuggingFace/.github/workflows/publish.yml ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Publish to Comfy registry
2
+ on:
3
+ workflow_dispatch:
4
+ push:
5
+ branches:
6
+ - main
7
+ - master
8
+ paths:
9
+ - "pyproject.toml"
10
+
11
+ permissions:
12
+ issues: write
13
+
14
+ jobs:
15
+ publish-node:
16
+ name: Publish Custom Node to registry
17
+ runs-on: ubuntu-latest
18
+ if: ${{ github.repository_owner == 'MoonGoblinDev' }}
19
+ steps:
20
+ - name: Check out code
21
+ uses: actions/checkout@v4
22
+ with:
23
+ submodules: true
24
+ - name: Publish Custom Node
25
+ uses: Comfy-Org/publish-node-action@v1
26
+ with:
27
+ ## Add your own personal access token to your Github Repository secrets and reference it here.
28
+ personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
custom_nodes/ComfyUI-HuggingFace/api/__init__.py ADDED
File without changes
custom_nodes/ComfyUI-HuggingFace/api/huggingface.py ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ================================================
2
+ # File: api/huggingface.py
3
+ # ================================================
4
+ import requests
5
+ import json
6
+ from typing import List, Optional, Dict, Any, Union
7
+
8
+ # Try to import huggingface_hub for better downloads
9
+ try:
10
+ from huggingface_hub import hf_hub_download, snapshot_download
11
+ HF_HUB_AVAILABLE = True
12
+ except ImportError:
13
+ HF_HUB_AVAILABLE = False
14
+ print("[HuggingFace API] huggingface_hub not available, falling back to manual downloads")
15
+
16
+ # Try to use huggingface_hub CLI as fallback
17
+ import subprocess
18
+ import sys
19
+
20
+ class HuggingFaceAPI:
21
+ """Simple wrapper for interacting with the HuggingFace API."""
22
+ BASE_URL = "https://huggingface.co/api"
23
+
24
+ def __init__(self, api_key: Optional[str] = None):
25
+ self.api_key = api_key
26
+ self.base_headers = {'Content-Type': 'application/json'}
27
+ if api_key:
28
+ self.base_headers["Authorization"] = f"Bearer {api_key}"
29
+ print("[HuggingFace API] Using HF token for private repositories.")
30
+ else:
31
+ print("[HuggingFace API] No HF token provided. Only public repositories accessible.")
32
+
33
+ def _get_request_headers(self, method: str, has_json_data: bool) -> Dict[str, str]:
34
+ """Returns headers for a specific request."""
35
+ headers = self.base_headers.copy()
36
+ # Don't send content-type for GET/HEAD if no json_data
37
+ if method.upper() in ["GET", "HEAD"] and not has_json_data:
38
+ headers.pop('Content-Type', None)
39
+ return headers
40
+
41
+ def _request(self, method: str, endpoint: str, params: Optional[Dict] = None,
42
+ json_data: Optional[Dict] = None, stream: bool = False,
43
+ allow_redirects: bool = True, timeout: int = 30) -> Union[Dict[str, Any], requests.Response, None]:
44
+ """Makes a request to the HuggingFace API and handles basic errors."""
45
+ url = f"{self.BASE_URL}/{endpoint.lstrip('/')}"
46
+ request_headers = self._get_request_headers(method, json_data is not None)
47
+
48
+ try:
49
+ response = requests.request(
50
+ method,
51
+ url,
52
+ headers=request_headers,
53
+ params=params,
54
+ json=json_data,
55
+ stream=stream,
56
+ allow_redirects=allow_redirects,
57
+ timeout=timeout
58
+ )
59
+ response.raise_for_status() # Raise HTTPError for bad responses (4xx or 5xx)
60
+
61
+ if stream:
62
+ return response # Return the response object for streaming
63
+
64
+ # Handle No Content response (e.g., 204)
65
+ if response.status_code == 204 or not response.content:
66
+ return None
67
+
68
+ return response.json()
69
+
70
+ except requests.exceptions.HTTPError as http_err:
71
+ error_detail = None
72
+ status_code = http_err.response.status_code
73
+ try:
74
+ error_detail = http_err.response.json()
75
+ except json.JSONDecodeError:
76
+ error_detail = http_err.response.text[:200] # First 200 chars
77
+ print(f"HuggingFace API HTTP Error ({method} {url}): Status {status_code}, Response: {error_detail}")
78
+ # Return a structured error dictionary
79
+ return {"error": f"HTTP Error: {status_code}", "details": error_detail, "status_code": status_code}
80
+
81
+ except requests.exceptions.RequestException as req_err:
82
+ print(f"HuggingFace API Request Error ({method} {url}): {req_err}")
83
+ return {"error": str(req_err), "details": None, "status_code": None}
84
+
85
+ except json.JSONDecodeError as json_err:
86
+ print(f"HuggingFace API Error: Failed to decode JSON response from {url}: {json_err}")
87
+ # Include response text if possible and not streaming
88
+ response_text = response.text[:200] if not stream and hasattr(response, 'text') else "N/A"
89
+ return {"error": "Invalid JSON response", "details": response_text, "status_code": response.status_code if hasattr(response, 'status_code') else None}
90
+
91
+ def search_models(self, query: str, limit: int = 20) -> Optional[Dict[str, Any]]:
92
+ """Searches for models on HuggingFace. (GET /models)"""
93
+ endpoint = "/models"
94
+ params = {
95
+ "search": query,
96
+ "limit": limit
97
+ }
98
+ result = self._request("GET", endpoint, params=params)
99
+ if isinstance(result, dict) and "error" in result:
100
+ return result
101
+ return result
102
+
103
+ def search_models_meili(self, query: str = None, types: Optional[List[str]] = None,
104
+ base_models: Optional[List[str]] = None,
105
+ sort: str = 'Most Downloaded', limit: int = 20, page: int = 1,
106
+ nsfw: Optional[bool] = None) -> Optional[Dict[str, Any]]:
107
+ """Searches models using HuggingFace's Meilisearch endpoint."""
108
+ meili_url = "https://huggingface.co/multi-search"
109
+ headers = {'Content-Type': 'application/json'}
110
+ if self.api_key:
111
+ headers['Authorization'] = f'Bearer {self.api_key}'
112
+
113
+ # Build search query
114
+ search_query = {
115
+ "q": query or "",
116
+ "limit": limit,
117
+ "offset": (page - 1) * limit
118
+ }
119
+
120
+ # Add filters
121
+ filters = []
122
+
123
+ # Type filters
124
+ if types:
125
+ type_filter = {"type": types}
126
+ filters.append(type_filter)
127
+
128
+ # Base model filters
129
+ if base_models:
130
+ base_filter = {"base_model": base_models}
131
+ filters.append(base_filter)
132
+
133
+ # NSFW filter
134
+ if nsfw is not None:
135
+ nsfw_filter = {"nsfw": nsfw}
136
+ filters.append(nsfw_filter)
137
+
138
+ if filters:
139
+ search_query["filters"] = filters
140
+
141
+ # Add sorting
142
+ sort_mapping = {
143
+ "Relevancy": "id:desc",
144
+ "Most Downloaded": "metrics.downloadCount:desc",
145
+ "Highest Rated": "metrics.thumbsUpCount:desc",
146
+ "Most Liked": "metrics.favoriteCount:desc",
147
+ "Most Discussed": "metrics.commentCount:desc",
148
+ "Most Collected": "metrics.collectedCount:desc",
149
+ "Most Buzz": "metrics.tippedAmountCount:desc",
150
+ "Newest": "createdAt:desc",
151
+ }
152
+
153
+ if sort in sort_mapping:
154
+ search_query["sort"] = [sort_mapping[sort]]
155
+
156
+ try:
157
+ response = requests.post(meili_url, json=search_query, headers=headers, timeout=30)
158
+ response.raise_for_status()
159
+ return response.json()
160
+ except requests.exceptions.RequestException as e:
161
+ print(f"HuggingFace Meili API Error: {e}")
162
+ return {"error": str(e), "status_code": getattr(e.response, 'status_code', None) if hasattr(e, 'response') else None}
163
+
164
+ def get_model_files(self, model_id: str) -> Optional[Dict[str, Any]]:
165
+ """Gets files for a specific HuggingFace model. Uses huggingface_hub instead of API."""
166
+ # Skip API calls, let huggingface_hub handle everything
167
+ print(f"[HuggingFace API] Skipping file listing, using huggingface_hub auto-detect for {model_id}")
168
+ return {"auto_detect": True}
169
+
170
+ def get_model_info(self, model_id: str) -> Optional[Dict[str, Any]]:
171
+ """Gets information about a model by its ID. Uses huggingface_hub instead of API."""
172
+ # Skip API calls, let huggingface_hub handle everything
173
+ print(f"[HuggingFace API] Skipping model info, using huggingface_hub auto-detect for {model_id}")
174
+ return {"id": model_id, "name": model_id.split('/')[-1]}
175
+
176
+ def get_model_version_info(self, version_id: str) -> Optional[Dict[str, Any]]:
177
+ """Gets version information - not applicable for HuggingFace, returns empty dict"""
178
+ # HuggingFace doesn't have version IDs like Civitai
179
+ # This method is kept for compatibility but returns empty
180
+ return {}
181
+
182
+ def download_file(self, model_id: str, filename: str, local_dir: str = None) -> Optional[Union[requests.Response, str]]:
183
+ """Downloads a specific file from HuggingFace. Uses only huggingface_hub."""
184
+ if not HF_HUB_AVAILABLE:
185
+ print("[HuggingFace API] huggingface_hub not available")
186
+ return None
187
+
188
+ if not local_dir:
189
+ print("[HuggingFace API] local_dir not specified")
190
+ return None
191
+
192
+ try:
193
+ print(f"[HuggingFace API] Using huggingface_hub for download: {model_id}/{filename}")
194
+
195
+ if filename is None:
196
+ # Download entire repo using snapshot_download
197
+ print(f"[HuggingFace API] Downloading entire repo {model_id}")
198
+ result = snapshot_download(
199
+ repo_id=model_id,
200
+ local_dir=local_dir,
201
+ token=self.api_key
202
+ )
203
+ print(f"[HuggingFace API] snapshot_download success: {result}")
204
+ return result
205
+ else:
206
+ # Download specific file using hf_hub_download
207
+ result = hf_hub_download(
208
+ repo_id=model_id,
209
+ filename=filename,
210
+ local_dir=local_dir,
211
+ resume_download=True,
212
+ token=self.api_key
213
+ )
214
+ print(f"[HuggingFace API] hf_hub_download success: {result}")
215
+ return result
216
+
217
+ except Exception as e:
218
+ print(f"[HuggingFace API] huggingface_hub download failed: {e}")
219
+ return None
custom_nodes/ComfyUI-HuggingFace/downloader/__init__.py ADDED
File without changes
custom_nodes/ComfyUI-HuggingFace/downloader/chunk_downloader.py ADDED
@@ -0,0 +1,574 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ================================================
2
+ # File: downloader/chunk_downloader.py
3
+ # Multi-connection somehow still not working
4
+ # ================================================
5
+
6
+ import requests
7
+ import threading
8
+ import time
9
+ import shutil
10
+ from pathlib import Path
11
+ import os
12
+ from typing import Optional, Dict, Tuple, Union, TYPE_CHECKING
13
+
14
+ # Import manager type hint without circular dependency during type checking
15
+ if TYPE_CHECKING:
16
+ from .manager import DownloadManager
17
+
18
+ # Import config values
19
+ from ..config import DEFAULT_CHUNK_SIZE, DOWNLOAD_TIMEOUT, HEAD_REQUEST_TIMEOUT
20
+
21
+ class ChunkDownloader:
22
+ """Handles downloading files in chunks using multiple connections or fallback."""
23
+ # Constants
24
+ STATUS_UPDATE_INTERVAL = 0.5
25
+ HEAD_REQUEST_TIMEOUT = HEAD_REQUEST_TIMEOUT
26
+ DOWNLOAD_TIMEOUT = DOWNLOAD_TIMEOUT
27
+ MIN_SIZE_FOR_MULTI_MB = 100 # Minimum file size for multi-connection download
28
+
29
+ def __init__(self, url: str, output_path: str, num_connections: int = 4,
30
+ chunk_size: int = DEFAULT_CHUNK_SIZE, manager: 'DownloadManager' = None,
31
+ download_id: str = None, api_key: Optional[str] = None,
32
+ known_size: Optional[int] = None):
33
+ # URLs
34
+ self.initial_url = url
35
+ self.url = url
36
+
37
+ # Paths
38
+ self.output_path = Path(output_path)
39
+ self.temp_dir = self.output_path.parent / f".{self.output_path.name}.parts_{download_id or int(time.time())}"
40
+
41
+ # Download configuration
42
+ self.num_connections = num_connections # Use the provided number of connections
43
+ self.chunk_size = chunk_size
44
+ self.manager = manager
45
+ self.download_id = download_id
46
+ self.api_key = api_key
47
+ self.known_size = known_size if known_size and known_size > 0 else None
48
+
49
+ # Download state
50
+ self.total_size = self.known_size or 0
51
+ self.downloaded = 0
52
+ self.connection_type = "N/A"
53
+ self.error = None
54
+
55
+ # Thread management
56
+ self.threads = []
57
+ self.lock = threading.Lock()
58
+ self.cancel_event = threading.Event()
59
+ self.part_files = []
60
+
61
+ # Performance tracking
62
+ self._start_time = 0
63
+ self._last_update_time = 0
64
+ self._last_downloaded_bytes = 0
65
+ self._speed = 0
66
+
67
+ def _get_request_headers(self, add_range: Optional[str] = None) -> Dict[str, str]:
68
+ """Constructs request headers with optional auth and range."""
69
+ headers = {}
70
+ if self.api_key:
71
+ headers["Authorization"] = f"Bearer {self.api_key}"
72
+ if add_range:
73
+ headers['Range'] = add_range
74
+ return headers
75
+
76
+ @property
77
+ def is_cancelled(self) -> bool:
78
+ """Check if download has been cancelled."""
79
+ return self.cancel_event.is_set()
80
+
81
+ def cancel(self):
82
+ """Signal the download to cancel."""
83
+ if not self.is_cancelled:
84
+ print(f"[Downloader {self.download_id or 'N/A'}] Cancellation requested by user.")
85
+ self.cancel_event.set()
86
+ self.error = "Download cancelled by user"
87
+ if self.manager and self.download_id:
88
+ self.manager._update_download_status(self.download_id, status="cancelled", error=self.error)
89
+
90
+ def _cleanup_temp(self, success: bool):
91
+ """Remove temporary directory and potentially the output file."""
92
+ # Clean up temp directory
93
+ if self.temp_dir.exists():
94
+ try:
95
+ shutil.rmtree(self.temp_dir)
96
+ except Exception as e:
97
+ print(f"[Downloader {self.download_id}] Warning: Could not remove temp directory {self.temp_dir}: {e}")
98
+
99
+ # Remove output file if download failed
100
+ if not success and self.output_path.exists():
101
+ try:
102
+ self.output_path.unlink()
103
+ print(f"[Downloader {self.download_id}] Removed incomplete/failed output file: {self.output_path}")
104
+ except Exception as e:
105
+ print(f"[Downloader {self.download_id}] Warning: Could not remove incomplete output file {self.output_path}: {e}")
106
+
107
+ def _get_range_support_and_url(self) -> Tuple[str, bool]:
108
+ """Check for range support and get final URL after redirects."""
109
+ final_url = self.initial_url
110
+ supports_ranges = False
111
+
112
+ try:
113
+ request_headers = self._get_request_headers()
114
+
115
+ print(f"[Downloader {self.download_id}] Checking range support/redirects for: {self.initial_url} (Timeout: {self.HEAD_REQUEST_TIMEOUT}s)")
116
+ response = requests.head(
117
+ self.initial_url,
118
+ allow_redirects=True,
119
+ timeout=self.HEAD_REQUEST_TIMEOUT,
120
+ headers=request_headers
121
+ )
122
+ response.raise_for_status()
123
+
124
+ # Update URL after redirects
125
+ final_url = response.url
126
+ self.url = final_url
127
+
128
+ # Check range support
129
+ accept_ranges = response.headers.get('accept-ranges', 'none').lower()
130
+ supports_ranges = accept_ranges == 'bytes'
131
+
132
+ # Get file size if not already known
133
+ if self.total_size <= 0:
134
+ head_size = int(response.headers.get('Content-Length', 0))
135
+ if head_size > 0:
136
+ self.total_size = head_size
137
+ print(f"[Downloader {self.download_id}] Got file size from HEAD: {self.total_size} bytes")
138
+
139
+ print(f"[Downloader {self.download_id}] HEAD Check OK - Final URL: {final_url}, Range Support: {supports_ranges}")
140
+ return final_url, supports_ranges
141
+
142
+ except requests.exceptions.Timeout:
143
+ print(f"[Downloader {self.download_id}] Warning: HEAD request timed out. Proceeding with Single connection.")
144
+ return self.initial_url, False
145
+
146
+ except requests.exceptions.RequestException as e:
147
+ http_error_details = ""
148
+ if hasattr(e, 'response') and e.response is not None:
149
+ status_code = e.response.status_code
150
+ http_error_details = f" (Status Code: {status_code})"
151
+ print(f"[Downloader {self.download_id}] Warning: HEAD request failed{http_error_details}. Proceeding with Single connection.")
152
+ return self.initial_url, False
153
+
154
+ except Exception as e:
155
+ print(f"[Downloader {self.download_id}] Warning: Unexpected error during HEAD request: {e}. Proceeding with Single connection.")
156
+ return self.initial_url, False
157
+
158
+ def _update_progress(self, chunk_len: int):
159
+ """Thread-safe update of download progress and speed calculation."""
160
+ with self.lock:
161
+ self.downloaded += chunk_len
162
+ current_time = time.monotonic()
163
+ time_diff = current_time - self._last_update_time
164
+
165
+ # Update speed and notify manager periodically
166
+ if time_diff >= self.STATUS_UPDATE_INTERVAL or self.downloaded == self.total_size:
167
+ progress = min((self.downloaded / self.total_size) * 100, 100.0) if self.total_size > 0 else 0
168
+
169
+ # Calculate speed
170
+ if time_diff > 0:
171
+ bytes_diff = self.downloaded - self._last_downloaded_bytes
172
+ self._speed = bytes_diff / time_diff
173
+
174
+ self._last_update_time = current_time
175
+ self._last_downloaded_bytes = self.downloaded
176
+
177
+ if self.manager and self.download_id:
178
+ self.manager._update_download_status(
179
+ self.download_id,
180
+ progress=progress,
181
+ speed=self._speed,
182
+ status="downloading"
183
+ )
184
+
185
+ def download_segment(self, segment_index: int, start_byte: int, end_byte: int):
186
+ """Downloads a specific segment of the file."""
187
+ part_file_path = self.temp_dir / f"part_{segment_index}"
188
+ request_headers = self._get_request_headers(add_range=f'bytes={start_byte}-{end_byte}')
189
+ retries = 3
190
+
191
+ for current_try in range(retries):
192
+ if self.is_cancelled:
193
+ print(f"[Downloader {self.download_id}] Segment {segment_index} cancelled before request (Try {current_try+1}).")
194
+ # Ensure error is set if not already
195
+ if not self.error: self.error = "Cancelled during segment download"
196
+ return
197
+
198
+ response = None
199
+ try:
200
+ response = requests.get(self.url, headers=request_headers, stream=True, timeout=self.DOWNLOAD_TIMEOUT)
201
+ response.raise_for_status()
202
+
203
+ bytes_written_this_segment = 0
204
+ with open(part_file_path, 'wb') as f:
205
+ for chunk in response.iter_content(self.chunk_size):
206
+ if self.is_cancelled:
207
+ print(f"[Downloader {self.download_id}] Segment {segment_index} cancelled mid-stream.")
208
+ # Ensure error is set if not already
209
+ if not self.error: self.error = "Cancelled during segment download"
210
+
211
+
212
+ if chunk:
213
+ bytes_written = f.write(chunk)
214
+ bytes_written_this_segment += bytes_written
215
+ self._update_progress(bytes_written)
216
+
217
+ # Verify segment size
218
+ expected_size = (end_byte - start_byte) + 1
219
+ if bytes_written_this_segment != expected_size:
220
+ if response:
221
+ response.close()
222
+ raise ValueError(f"Size mismatch. Expected {expected_size}, got {bytes_written_this_segment}")
223
+
224
+ return # Success
225
+
226
+
227
+ except (requests.exceptions.RequestException, ValueError) as e:
228
+ # Handle HTTP status codes
229
+ status_code = None
230
+ error_msg_detail = f"{e}"
231
+
232
+ if isinstance(e, requests.exceptions.RequestException) and hasattr(e, 'response') and e.response is not None:
233
+ status_code = e.response.status_code
234
+ if status_code == 401:
235
+ error_msg_detail += " (Unauthorized)"
236
+ elif status_code == 403:
237
+ error_msg_detail += " (Forbidden)"
238
+ elif status_code == 416:
239
+ error_msg_detail += " (Range Not Satisfiable)"
240
+ self.error = f"Segment {segment_index} failed: {error_msg_detail}"
241
+ self.cancel()
242
+ return
243
+
244
+ print(f"[Downloader {self.download_id}] Warning: Segment {segment_index} failed (Try {current_try+1}/{retries}): {error_msg_detail}")
245
+
246
+ if current_try >= retries - 1: # Last attempt failed
247
+ self.error = f"Segment {segment_index} failed after {retries} attempts: {error_msg_detail}"
248
+ self.cancel()
249
+ return
250
+
251
+ # Exponential backoff before retry
252
+ time.sleep(min(2 ** current_try, 10))
253
+
254
+ except Exception as e:
255
+ self.error = f"Segment {segment_index} critical error: {e}"
256
+ print(f"[Downloader {self.download_id}] Error: {self.error}")
257
+ self.cancel()
258
+ return
259
+
260
+ finally:
261
+ if response:
262
+ response.close()
263
+
264
+ def merge_parts(self) -> bool:
265
+ """Merges all downloaded part files into the final output file."""
266
+ print(f"[Downloader {self.download_id}] Merging {len(self.part_files)} parts for {self.output_path.name}...")
267
+
268
+ # Check if we have parts to merge
269
+ if not self.part_files:
270
+ if self.is_cancelled:
271
+ self.error = self.error or "Cancelled before any parts downloaded."
272
+ elif not self.error:
273
+ self.error = "No part files were created to merge."
274
+ print(f"[Downloader {self.download_id}] Error during merge: {self.error}")
275
+ return False
276
+
277
+ try:
278
+ # Sort part files numerically
279
+ sorted_part_files = sorted(self.part_files, key=lambda p: int(p.name.split('_')[-1]))
280
+
281
+ with open(self.output_path, 'wb') as outfile:
282
+ for part_file in sorted_part_files:
283
+ # Check if part exists
284
+ if not part_file.exists():
285
+ if self.is_cancelled:
286
+ self.error = self.error or "Cancelled during download, a part file is missing."
287
+ elif not self.error:
288
+ self.error = f"Merge failed, required part file is missing: {part_file}."
289
+ print(f"[Downloader {self.download_id}] Warning: Aborting merge. Missing part: {part_file.name}.")
290
+ return False
291
+
292
+ # Copy part data to output file
293
+ try:
294
+ with open(part_file, 'rb') as infile:
295
+ shutil.copyfileobj(infile, outfile, length=1024*1024*2) # 2MB buffer
296
+ except Exception as copy_e:
297
+ self.error = f"Error copying data from part {part_file.name} during merge: {copy_e}"
298
+ print(f"[Downloader {self.download_id}] Error: {self.error}")
299
+ return False
300
+
301
+ print(f"[Downloader {self.download_id}] Merging complete.")
302
+
303
+ # Verify final file size
304
+ final_size = self.output_path.stat().st_size
305
+ if self.total_size > 0 and abs(final_size - self.total_size) > 1:
306
+ self.error = f"Merged size ({final_size}) differs significantly from expected ({self.total_size}). File may be corrupt."
307
+ print(f"[Downloader {self.download_id}] Error: {self.error}")
308
+ return False
309
+ elif self.total_size > 0 and final_size != self.total_size:
310
+ print(f"[Downloader {self.download_id}] Warning: Final merged size ({final_size}) differs slightly from expected ({self.total_size}).")
311
+
312
+ return True
313
+
314
+ except Exception as e:
315
+ self.error = f"Failed to merge parts: {e}"
316
+ print(f"[Downloader {self.download_id}] Error: {self.error}")
317
+ return False
318
+
319
+ def fallback_download(self) -> bool:
320
+ """Fallback to standard single-connection download."""
321
+ self.connection_type = "Single"
322
+ if self.manager and self.download_id:
323
+ self.manager._update_download_status(self.download_id, connection_type=self.connection_type, status="downloading")
324
+
325
+ print(f"[Downloader {self.download_id}] Using standard single-connection download for {self.output_path.name}...")
326
+
327
+ self._start_time = self._start_time or time.monotonic()
328
+ self._last_update_time = self._start_time
329
+ self._last_downloaded_bytes = 0
330
+ self.downloaded = 0
331
+
332
+ response = None
333
+
334
+ try:
335
+ request_headers = self._get_request_headers()
336
+ response = requests.get(self.url, stream=True, timeout=self.DOWNLOAD_TIMEOUT,
337
+ allow_redirects=True, headers=request_headers)
338
+ response.raise_for_status()
339
+
340
+ # Update URL after potential redirects
341
+ final_get_url = response.url
342
+ if final_get_url != self.url:
343
+ print(f"[Downloader {self.download_id}] URL redirected during GET to: {final_get_url}")
344
+ self.url = final_get_url
345
+
346
+ # Get/confirm file size
347
+ if self.total_size <= 0:
348
+ get_size = int(response.headers.get('Content-Length', 0))
349
+ if get_size > 0:
350
+ self.total_size = get_size
351
+ print(f"[Downloader {self.download_id}] Obtained file size via fallback GET: {self.total_size}")
352
+ else:
353
+ print(f"[Downloader {self.download_id}] Warning: File size unknown. Progress may be inaccurate.")
354
+
355
+ # Ensure output directory exists
356
+ self.output_path.parent.mkdir(parents=True, exist_ok=True)
357
+
358
+ with open(self.output_path, 'wb') as f:
359
+ for chunk in response.iter_content(self.chunk_size):
360
+ if self.is_cancelled:
361
+ print(f"[Downloader {self.download_id}] Fallback download cancelled.")
362
+ return False
363
+ if chunk:
364
+ bytes_written = f.write(chunk)
365
+ self._update_progress(bytes_written)
366
+
367
+
368
+
369
+ # Verify download size if known
370
+ if self.total_size > 0 and self.downloaded != self.total_size and not self.error:
371
+ print(f"[Downloader {self.download_id}] Warning: Size mismatch. Expected {self.total_size}, got {self.downloaded}.")
372
+
373
+ print(f"[Downloader {self.download_id}] Fallback download completed.")
374
+ return not self.error
375
+
376
+ except requests.exceptions.RequestException as e:
377
+ error_msg_detail = f"{e}"
378
+ if hasattr(e, 'response') and e.response is not None:
379
+ status_code = e.response.status_code
380
+ if status_code == 401:
381
+ error_msg_detail += " (Unauthorized - Check HF token?)"
382
+ elif status_code == 403:
383
+ error_msg_detail += " (Forbidden - Permissions Issue?)"
384
+
385
+ if not self.error:
386
+ self.error = f"Fallback download failed: {error_msg_detail}"
387
+ print(f"[Downloader {self.download_id}] Error during fallback download: {self.error}")
388
+ return False
389
+
390
+ except Exception as e:
391
+ if not self.error:
392
+ self.error = f"Fallback download failed: {e}"
393
+ print(f"[Downloader {self.download_id}] Error during fallback download: {self.error}")
394
+ return False
395
+
396
+ finally:
397
+ if response:
398
+ response.close()
399
+
400
+ def download(self) -> bool:
401
+ """Main download method that chooses between multi-connection or fallback approach."""
402
+ self._start_time = time.monotonic()
403
+ self.downloaded = 0
404
+ self.error = None
405
+ self.threads = []
406
+ self.part_files = []
407
+ success = False
408
+
409
+ # Clean up any existing temp directory
410
+ if self.temp_dir.exists():
411
+ print(f"[Downloader {self.download_id}] Warning: Removing leftover temp directory: {self.temp_dir}")
412
+ self._cleanup_temp(success=False)
413
+
414
+ # Check range support and get final URL
415
+ final_url, supports_ranges = self._get_range_support_and_url()
416
+
417
+ # Decide on download strategy
418
+ use_multi_connection = False
419
+ if supports_ranges and self.num_connections > 1 and self.total_size > 0:
420
+ if self.total_size > self.MIN_SIZE_FOR_MULTI_MB * 1024 * 1024:
421
+ use_multi_connection = True
422
+ else:
423
+ print(f"[Downloader {self.download_id}] File size ({self.total_size / (1024*1024):.2f} MB) below threshold for multi-connection.")
424
+
425
+ expected_final_size = self.total_size
426
+
427
+ try:
428
+ if use_multi_connection:
429
+ # Multi-connection download approach
430
+ success = self._do_multi_connection_download()
431
+ else:
432
+ # Single connection fallback
433
+ reason = "Range requests not supported" if not supports_ranges else \
434
+ "Single connection requested" if self.num_connections <= 1 else \
435
+ "File size unknown or too small"
436
+ print(f"[Downloader {self.download_id}] ({reason}). Using fallback single-connection download.")
437
+ success = self.fallback_download()
438
+ expected_final_size = self.total_size
439
+
440
+ if not success and not self.error:
441
+ self.error = "Single connection download failed."
442
+
443
+ except KeyboardInterrupt:
444
+ print(f"[Downloader {self.download_id}] Interrupted! Signalling cancellation.")
445
+ self.cancel()
446
+ if not self.error:
447
+ self.error = "Download interrupted by user."
448
+ success = False
449
+
450
+ except Exception as e:
451
+ import traceback
452
+ print(f"--- Critical Error in Download {self.download_id} ('{self.output_path.name}') ---")
453
+ traceback.print_exc()
454
+ print("--- End Error ---")
455
+
456
+ if not self.error:
457
+ self.error = f"Unexpected download error: {str(e)}"
458
+
459
+ success = False
460
+ if not self.is_cancelled:
461
+ self.cancel()
462
+
463
+ finally:
464
+ # Cleanup and final status update
465
+ self._cleanup_temp(success=success and not self.is_cancelled and not self.error)
466
+
467
+ if self.manager and self.download_id:
468
+ final_status = "completed" if success else ("cancelled" if self.is_cancelled else "failed")
469
+ final_progress = 100.0 if success else ((self.downloaded / self.total_size * 100) if self.total_size > 0 else 0)
470
+
471
+ self.manager._update_download_status(
472
+ self.download_id,
473
+ status=final_status,
474
+ progress=min(100.0, final_progress),
475
+ speed=0,
476
+ error=self.error,
477
+ connection_type=self.connection_type
478
+ )
479
+
480
+ return success and not self.error and not self.is_cancelled
481
+
482
+ def _do_multi_connection_download(self) -> bool:
483
+ """Handle multi-connection download process."""
484
+ self.connection_type = f"Multi ({self.num_connections})"
485
+ if self.manager and self.download_id:
486
+ self.manager._update_download_status(self.download_id, connection_type=self.connection_type, status="downloading")
487
+
488
+ print(f"[Downloader {self.download_id}] Starting multi-connection download for {self.output_path.name} "
489
+ f"({self.total_size / (1024 * 1024):.2f} MB) using {self.num_connections} connections.")
490
+
491
+ # Create temp directory
492
+ try:
493
+ if self.temp_dir.exists():
494
+ shutil.rmtree(self.temp_dir)
495
+ self.temp_dir.mkdir(parents=True)
496
+ except Exception as e:
497
+ self.error = f"Failed to create temp directory: {e}"
498
+ print(f"[Downloader {self.download_id}] Error: {self.error}")
499
+ return False
500
+
501
+ # Calculate segments
502
+ segment_size = self.total_size // self.num_connections
503
+
504
+ # Handle small files with many connections
505
+ if segment_size == 0 and self.total_size > 0:
506
+ segment_size = self.total_size // min(self.num_connections, self.total_size) if self.total_size >= self.num_connections else self.total_size
507
+ if segment_size == 0:
508
+ segment_size = self.total_size
509
+ self.num_connections = 1
510
+ print(f"[Downloader {self.download_id}] Warning: Forcing single connection for very small file.")
511
+ return self.fallback_download()
512
+
513
+ # Create segments
514
+ segments = []
515
+ current_byte = 0
516
+ for i in range(self.num_connections):
517
+ if current_byte >= self.total_size:
518
+ break
519
+
520
+ start_byte = current_byte
521
+ end_byte = min(current_byte + segment_size - 1, self.total_size - 1)
522
+
523
+ # Ensure last segment goes to the end
524
+ if i == self.num_connections - 1:
525
+ end_byte = self.total_size - 1
526
+
527
+ # Ensure segment is valid
528
+ if start_byte <= end_byte < self.total_size:
529
+ segments.append((i, start_byte, end_byte))
530
+ self.part_files.append(self.temp_dir / f"part_{i}")
531
+ else:
532
+ print(f"[Downloader {self.download_id}] Warning: Skipping invalid segment {i}, start={start_byte}, end={end_byte}")
533
+
534
+ current_byte = end_byte + 1
535
+
536
+ if not segments:
537
+ self.error = f"No valid download segments calculated (Total Size: {self.total_size})."
538
+ print(f"[Downloader {self.download_id}] Error: {self.error}")
539
+ return False
540
+
541
+ # Start download threads
542
+ for index, start, end in segments:
543
+ if self.is_cancelled:
544
+ break
545
+ thread = threading.Thread(target=self.download_segment, args=(index, start, end), daemon=True)
546
+ self.threads.append(thread)
547
+ thread.start()
548
+
549
+ # Wait for threads to complete
550
+ active_threads = list(self.threads)
551
+ while active_threads and not self.is_cancelled:
552
+ joined_threads = []
553
+ for t in active_threads:
554
+ t.join(timeout=0.2)
555
+ if not t.is_alive():
556
+ joined_threads.append(t)
557
+
558
+ active_threads = [t for t in active_threads if t not in joined_threads]
559
+
560
+ # Handle download completion
561
+ if self.is_cancelled:
562
+ print(f"[Downloader {self.download_id}] Download stopped (cancelled).")
563
+ self.error = self.error or "Download cancelled."
564
+ return False
565
+ elif self.error:
566
+ print(f"[Downloader {self.download_id}] Download stopped (error): {self.error}")
567
+ return False
568
+ elif self.total_size > 0 and self.downloaded != self.total_size:
569
+ self.error = f"Multi-download size mismatch. Expected {self.total_size}, got {self.downloaded}."
570
+ print(f"[Downloader {self.download_id}] Error: {self.error}")
571
+ return False
572
+
573
+ # Merge parts
574
+ return self.merge_parts()
custom_nodes/ComfyUI-HuggingFace/downloader/manager.py ADDED
@@ -0,0 +1,996 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ================================================
2
+ # File: downloader/manager.py
3
+ # ================================================
4
+ import threading
5
+ import time
6
+ import datetime
7
+ import os
8
+ import json
9
+ import requests
10
+ import subprocess
11
+ import platform
12
+ import sys
13
+ from typing import List, Dict, Any, Optional, TYPE_CHECKING
14
+
15
+ if TYPE_CHECKING:
16
+ from .chunk_downloader import ChunkDownloader
17
+
18
+ from ..config import (
19
+ MAX_CONCURRENT_DOWNLOADS, DOWNLOAD_HISTORY_LIMIT, DEFAULT_CONNECTIONS,
20
+ METADATA_SUFFIX, PREVIEW_SUFFIX, METADATA_DOWNLOAD_TIMEOUT, PLUGIN_ROOT
21
+ )
22
+ try:
23
+ from folder_paths import get_directory_by_type, get_valid_path, base_path
24
+ COMFY_PATHS_AVAILABLE = True
25
+ except ImportError:
26
+ print("[HuggingFace Manager] Warning: ComfyUI folder_paths not available. Path validation/opening might be limited.")
27
+ COMFY_PATHS_AVAILABLE = False
28
+ base_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
29
+
30
+ # --- Define History File Path ---
31
+ # Place it in the root of the extension directory
32
+ HISTORY_FILE_PATH = os.path.join(PLUGIN_ROOT, "download_history.json")
33
+
34
+ class DownloadManager:
35
+ """Manages a queue of downloads, running them concurrently and saving metadata."""
36
+
37
+ def __init__(self, max_concurrent: int = MAX_CONCURRENT_DOWNLOADS):
38
+ self.queue: List[Dict[str, Any]] = []
39
+ self.active_downloads: Dict[str, Dict[str, Any]] = {} # {download_id: download_info}
40
+ # History now stores more complete dictionaries for retry functionality
41
+ self.history: List[Dict[str, Any]] = []
42
+ self.lock: threading.Lock = threading.Lock()
43
+ self.max_concurrent: int = max(1, max_concurrent)
44
+ self.running: bool = True
45
+ self._load_history_from_file()
46
+ self._process_thread: threading.Thread = threading.Thread(target=self._process_queue, daemon=True)
47
+ print(f"HuggingFace Download Manager starting (Max Concurrent: {self.max_concurrent}).")
48
+ self._process_thread.start()
49
+
50
+ # --- add_to_queue remains largely the same, ensuring all necessary fields are initialized ---
51
+ def add_to_queue(self, download_info: Dict[str, Any]) -> str:
52
+ """Adds a download task to the queue."""
53
+ with self.lock:
54
+ # Generate a unique ID
55
+ timestamp = int(time.time() * 1000)
56
+ file_hint = os.path.basename(download_info.get('output_path', 'file'))[:10]
57
+ unique_num = sum(1 for item in self.queue if file_hint in item.get("id", "") or any(file_hint in h.get("id","") for h in self.history)) # Check history too
58
+ download_id = f"dl_{timestamp}_{unique_num}_{file_hint}"
59
+
60
+ # Set initial status and info
61
+ download_info["id"] = download_id
62
+ download_info["status"] = "queued"
63
+ download_info["added_time"] = datetime.datetime.now(datetime.timezone.utc).isoformat()
64
+ download_info["progress"] = 0
65
+ download_info["speed"] = 0
66
+ download_info["error"] = None
67
+ download_info["start_time"] = None
68
+ download_info["end_time"] = None
69
+ download_info["connection_type"] = "N/A"
70
+
71
+ # --- Ensure all fields potentially needed for retry exist ---
72
+ # (Most were likely already filled by the calling route, but double-check)
73
+ required_for_retry = [
74
+ 'url', 'output_path', 'num_connections', 'api_key', 'known_size',
75
+ 'huggingface_model_info', 'huggingface_version_info', 'huggingface_primary_file',
76
+ 'thumbnail', 'filename', 'model_url_or_id', 'model_version_id', 'model_type',
77
+ 'custom_filename', 'force_redownload', 'huggingface_model_name' # Add huggingface_model_name!
78
+ ]
79
+ for key in required_for_retry:
80
+ if key not in download_info:
81
+ # Add default or None if missing. More robust handling might be needed
82
+ # depending on how routes.py prepares the dict.
83
+ if key in ['huggingface_model_info', 'huggingface_version_info', 'huggingface_primary_file']:
84
+ download_info[key] = {}
85
+ elif key == 'num_connections':
86
+ download_info[key] = DEFAULT_CONNECTIONS
87
+ elif key == 'force_redownload':
88
+ download_info[key] = False
89
+ elif key == 'huggingface_model_name':
90
+ download_info[key] = None
91
+ else:
92
+ download_info[key] = None
93
+ print(f"[Manager Warning] Queued item '{download_id}' missing '{key}', added default.")
94
+
95
+ self.queue.append(download_info)
96
+ print(f"[Manager] Queued: {download_info.get('filename', 'N/A')} (ID: {download_id}, Size: {download_info.get('known_size', 'Unknown')})")
97
+ return download_id
98
+
99
+ # --- cancel_download remains the same ---
100
+ def cancel_download(self, download_id: str) -> bool:
101
+ """Requests cancellation of a queued or active download."""
102
+ # ... (no changes needed here) ...
103
+ print(f"[Manager] Received cancellation request for: {download_id}") # Moved print earlier
104
+ downloader_to_cancel: Optional['ChunkDownloader'] = None
105
+ found_in_active = False
106
+
107
+ with self.lock:
108
+ # 1. Check queue first (can be fully handled under lock)
109
+ for i, item in enumerate(self.queue):
110
+ if item["id"] == download_id:
111
+ cancelled_info = self.queue.pop(i)
112
+ cancelled_info["status"] = "cancelled"
113
+ cancelled_info["end_time"] = datetime.datetime.now(datetime.timezone.utc).isoformat()
114
+ cancelled_info["error"] = "Cancelled from queue"
115
+ self._add_to_history(cancelled_info) # _add_to_history is safe under lock
116
+ print(f"[Manager] Cancelled queued download: {download_id}")
117
+ return True # Cancelled from queue, we are done
118
+
119
+ # 2. Check active downloads - Find the instance *under lock*
120
+ if download_id in self.active_downloads:
121
+ found_in_active = True
122
+ active_info = self.active_downloads[download_id]
123
+ downloader_to_cancel = active_info.get("downloader_instance")
124
+ current_status = active_info.get("status")
125
+
126
+ # If downloader instance doesn't exist yet (status 'starting')
127
+ # or if already terminal, handle it here under lock
128
+ if not downloader_to_cancel and current_status == "starting":
129
+ print(f"[Manager] Marking 'starting' download as cancelled: {download_id}")
130
+ # Mark as cancelled, it won't start or will be caught by wrapper
131
+ active_info["status"] = "cancelled"
132
+ if not active_info.get("end_time"):
133
+ active_info["end_time"] = datetime.datetime.now(datetime.timezone.utc).isoformat()
134
+ active_info["error"] = "Cancelled before download thread fully started"
135
+ # Don't assign downloader_to_cancel, let process_queue clean it up
136
+ downloader_to_cancel = None # Explicitly clear
137
+ # Indicate we handled it (or attempted to)
138
+ return True # Exit, processed within lock
139
+ elif current_status in ["completed", "failed", "cancelled"]:
140
+ print(f"[Manager] Download {download_id} is already in terminal state '{current_status}'. Cannot cancel.")
141
+ # Indicate we don't need to proceed further outside the lock
142
+ downloader_to_cancel = None # Explicitly clear
143
+ return False # Already finished/cancelled
144
+
145
+ # If we found an instance and it's potentially running,
146
+ # store it to call cancel *after* releasing the lock.
147
+ print(f"[Manager] Found active downloader instance for {download_id}. Will signal outside lock.")
148
+
149
+ # Lock is released automatically here when exiting 'with'
150
+
151
+ # 3. Signal the downloader instance *outside* the lock
152
+ if downloader_to_cancel:
153
+ try:
154
+ # Check if already cancelled (to avoid duplicate logs/actions) - This check is thread-safe
155
+ if not downloader_to_cancel.is_cancelled:
156
+ print(f"[Manager] Calling downloader.cancel() for {download_id}")
157
+ downloader_to_cancel.cancel() # This can now call _update_download_status safely
158
+ print(f"[Manager] Signalled downloader.cancel() for {download_id}")
159
+ # Let the download thread's final status update handle moving to history
160
+ return True # Signal sent
161
+ else:
162
+ print(f"[Manager] Active download {download_id} was already cancelling.")
163
+ return True # Already in cancelling state is considered a success here
164
+ except Exception as e:
165
+ print(f"[Manager] Error calling downloader.cancel() for {download_id}: {e}")
166
+ # Update status to failed maybe? Or just log.
167
+ # Use _update_download_status directly here as we are outside the lock
168
+ self._update_download_status(download_id, status="failed", error=f"Error during cancel signaling: {e}")
169
+ return False # Failed to signal
170
+
171
+ # 4. Handle cases where it wasn't in queue and wasn't running/starting
172
+ if not found_in_active:
173
+ print(f"[Manager] Could not cancel - ID not found in queue or active: {download_id}")
174
+ return False # Not found
175
+
176
+ # It was found in active but was already terminal or couldn't be signalled
177
+ # Return value determined above
178
+ return False # Should have returned True earlier if successful
179
+
180
+ # --- get_status remains the same (still strips data for UI) ---
181
+ def get_status(self) -> Dict[str, List[Dict[str, Any]]]:
182
+ """Returns the current state of the queue, active downloads, and history.
183
+ Strips sensitive/large data for UI efficiency."""
184
+ with self.lock:
185
+ # Fields to exclude when sending status to UI
186
+ exclude_fields_for_ui = [
187
+ 'downloader_instance', 'huggingface_model_info', 'huggingface_version_info',
188
+ 'api_key', # Don't send API key to frontend status
189
+ # Large potentially redundant fields:
190
+ 'url', 'output_path', 'custom_filename', 'model_url_or_id',
191
+ # Keep 'thumbnail', 'filename', 'model_name', 'version_name' etc for display
192
+ ]
193
+
194
+ # Prepare active downloads list
195
+ active_list = [
196
+ {k: v for k, v in item_data.items() if k not in exclude_fields_for_ui}
197
+ for item_id, item_data in self.active_downloads.items()
198
+ ]
199
+
200
+ # Prepare history list similarly
201
+ history_list = [
202
+ {k: v for k, v in item_data.items() if k not in exclude_fields_for_ui}
203
+ for item_data in self.history[:DOWNLOAD_HISTORY_LIMIT]
204
+ ]
205
+
206
+ # Return copies
207
+ return {
208
+ "queue": [
209
+ {k:v for k,v in item.items() if k not in exclude_fields_for_ui}
210
+ for item in self.queue
211
+ ],
212
+ "active": active_list,
213
+ "history": history_list,
214
+ }
215
+
216
+ def _load_history_from_file(self):
217
+ """Loads download history from the JSON file."""
218
+ # No lock needed here as it's called during __init__ before the thread starts
219
+ if not os.path.exists(HISTORY_FILE_PATH):
220
+ print(f"[Manager] History file not found ({HISTORY_FILE_PATH}). Starting with empty history.")
221
+ self.history = []
222
+ return
223
+
224
+ try:
225
+ with open(HISTORY_FILE_PATH, 'r', encoding='utf-8') as f:
226
+ loaded_data = json.load(f)
227
+
228
+ if isinstance(loaded_data, list):
229
+ # Basic validation: Ensure items have IDs (optional but good)
230
+ validated_history = [item for item in loaded_data if isinstance(item, dict) and 'id' in item]
231
+ invalid_count = len(loaded_data) - len(validated_history)
232
+ if invalid_count > 0:
233
+ print(f"[Manager Warning] {invalid_count} items removed from loaded history due to missing 'id'.")
234
+
235
+ # Ensure history limit
236
+ self.history = validated_history[:DOWNLOAD_HISTORY_LIMIT]
237
+ print(f"[Manager] Successfully loaded {len(self.history)} items from {HISTORY_FILE_PATH}.")
238
+ else:
239
+ print(f"[Manager Warning] History file ({HISTORY_FILE_PATH}) contained invalid data (not a list). Starting fresh.")
240
+ self.history = []
241
+ # Optionally try to delete the corrupted file?
242
+ # try: os.remove(HISTORY_FILE_PATH) except Exception: pass
243
+
244
+ except json.JSONDecodeError as e:
245
+ print(f"[Manager Error] Failed to parse history file ({HISTORY_FILE_PATH}): {e}. Starting fresh.")
246
+ self.history = []
247
+ # Optionally try to delete the corrupted file?
248
+ except Exception as e:
249
+ print(f"[Manager Error] Failed to read history file ({HISTORY_FILE_PATH}): {e}. Starting fresh.")
250
+ self.history = []
251
+
252
+ # --- Save History Method ---
253
+ def _save_history_to_file(self):
254
+ """Saves the current in-memory history list to the JSON file."""
255
+ # Assumes self.lock is HELD when this is called
256
+ history_to_save = self.history[:DOWNLOAD_HISTORY_LIMIT] # Ensure limit before saving
257
+
258
+ try:
259
+ # Ensure directory exists (should already, but belt-and-suspenders)
260
+ os.makedirs(os.path.dirname(HISTORY_FILE_PATH), exist_ok=True)
261
+
262
+ # Write atomically (write to temp then rename) to reduce corruption risk
263
+ temp_file_path = HISTORY_FILE_PATH + ".tmp"
264
+ with open(temp_file_path, 'w', encoding='utf-8') as f:
265
+ json.dump(history_to_save, f, indent=2, ensure_ascii=False) # Pretty print
266
+
267
+ os.replace(temp_file_path, HISTORY_FILE_PATH) # Atomic rename/replace
268
+ # print(f"[Manager] Saved {len(history_to_save)} items to history file.") # Can be noisy
269
+
270
+ except Exception as e:
271
+ # Log error, but don't crash the manager
272
+ print(f"[Manager Error] Failed to save history file ({HISTORY_FILE_PATH}): {e}")
273
+ # Attempt to remove temp file if it exists
274
+ if os.path.exists(temp_file_path):
275
+ try: os.remove(temp_file_path)
276
+ except Exception: pass
277
+
278
+ # --- Updated _add_to_history Method ---
279
+ def _add_to_history(self, download_info: Dict[str, Any]):
280
+ """Adds a completed/failed/cancelled item to history (internal).
281
+ Stores most original parameters needed for potential retry.
282
+ NOW ALSO TRIGGERS SAVING HISTORY TO FILE."""
283
+ # --- (Keep existing logic to prepare info_copy) ---
284
+ info_copy = {
285
+ k: v for k, v in download_info.items()
286
+ if k not in ['downloader_instance']
287
+ }
288
+ if "end_time" not in info_copy or info_copy["end_time"] is None:
289
+ info_copy["end_time"] = datetime.datetime.now(datetime.timezone.utc).isoformat()
290
+ if "status" not in info_copy:
291
+ info_copy["status"] = "unknown"
292
+ if info_copy["status"] != "completed" and not info_copy.get("error"):
293
+ info_copy["error"] = f"Finished with status '{info_copy['status']}' but no error recorded."
294
+
295
+ # --- Update in-memory history ---
296
+ self.history.insert(0, info_copy) # Prepend
297
+ # Trim in-memory list (save will also respect limit)
298
+ if len(self.history) > DOWNLOAD_HISTORY_LIMIT:
299
+ self.history = self.history[:DOWNLOAD_HISTORY_LIMIT]
300
+
301
+ # --- Trigger Save to File (still under lock) ---
302
+ self._save_history_to_file() # <--- Added call
303
+
304
+ # --- Updated clear_history Method ---
305
+ def clear_history(self) -> Dict[str, Any]:
306
+ """Clears the download history (in-memory and the JSON file)."""
307
+ cleared_count = 0
308
+ file_deleted = False
309
+ error_msg = None
310
+
311
+ try:
312
+ with self.lock:
313
+ cleared_count = len(self.history)
314
+ if cleared_count > 0:
315
+ print(f"[Manager] Clearing {cleared_count} items from in-memory history.")
316
+ self.history = [] # Clear memory list first
317
+
318
+ # Attempt to delete the history file
319
+ if os.path.exists(HISTORY_FILE_PATH):
320
+ try:
321
+ os.remove(HISTORY_FILE_PATH)
322
+ file_deleted = True
323
+ print(f"[Manager] Deleted history file: {HISTORY_FILE_PATH}")
324
+ except OSError as e:
325
+ error_msg = f"Failed to delete history file {HISTORY_FILE_PATH}: {e}"
326
+ print(f"[Manager Error] {error_msg}")
327
+ else:
328
+ print(f"[Manager] History file ({HISTORY_FILE_PATH}) did not exist, nothing to delete.")
329
+ file_deleted = True # Consider success if file wasn't there anyway
330
+
331
+ else:
332
+ print("[Manager] History clear request received, but history was already empty.")
333
+ # Should we still check/delete the file just in case? Yes.
334
+ if os.path.exists(HISTORY_FILE_PATH):
335
+ try:
336
+ os.remove(HISTORY_FILE_PATH)
337
+ file_deleted = True
338
+ print(f"[Manager] Deleted potentially orphaned history file: {HISTORY_FILE_PATH}")
339
+ except OSError as e:
340
+ error_msg = f"Failed to delete potentially orphaned history file {HISTORY_FILE_PATH}: {e}"
341
+ print(f"[Manager Error] {error_msg}")
342
+ else:
343
+ file_deleted = True # Success if clear requested and neither memory/file had anything
344
+
345
+ if error_msg:
346
+ return {"success": False, "error": f"History cleared from memory, but could not delete file: {error_msg}"}
347
+ elif cleared_count > 0:
348
+ return {"success": True, "message": f"Cleared {cleared_count} history items (memory and file)."}
349
+ else:
350
+ # If count was 0 but file deletion was attempted/succeeded
351
+ return {"success": True, "message": "History was already empty."}
352
+
353
+ except Exception as e:
354
+ print(f"[Manager] Critical error during clear_history: {e}")
355
+ import traceback
356
+ traceback.print_exc()
357
+ return {"success": False, "error": f"Failed to clear history due to unexpected error: {e}"}
358
+
359
+ # --- _process_queue remains the same ---
360
+ def _process_queue(self):
361
+ """Internal thread function to manage downloads."""
362
+ # ... (no changes needed here) ...
363
+ print("[Manager] Process queue thread started.")
364
+ while self.running:
365
+ processed_something = False
366
+ with self.lock:
367
+ # 1. Check for finished/failed/cancelled active downloads to move to history
368
+ finished_ids = [
369
+ dl_id for dl_id, info in self.active_downloads.items()
370
+ if info.get("status") in ["completed", "failed", "cancelled"] # Use .get() for safety
371
+ ]
372
+ for dl_id in finished_ids:
373
+ # Check if still in active_downloads before popping (might have been removed by another thread edge case?)
374
+ if dl_id in self.active_downloads:
375
+ finished_info = self.active_downloads.pop(dl_id)
376
+ self._add_to_history(finished_info) # Will now store more data
377
+ print(f"[Manager] Moved '{finished_info.get('filename', dl_id)}' to history (Status: {finished_info['status']})")
378
+ processed_something = True
379
+ else:
380
+ print(f"[Manager] Warning: Item {dl_id} intended for history was already removed from active list.")
381
+
382
+ # 2. Start new downloads if slots available and queue has items
383
+ while len(self.active_downloads) < self.max_concurrent and self.queue:
384
+ download_info = self.queue.pop(0)
385
+ download_id = download_info["id"]
386
+
387
+ # Double check if cancelled just before starting
388
+ if download_info["status"] == "cancelled":
389
+ # Ensure it has an end time before adding to history
390
+ if not download_info.get("end_time"):
391
+ download_info["end_time"] = datetime.datetime.now(datetime.timezone.utc).isoformat()
392
+ self._add_to_history(download_info)
393
+ print(f"[Manager] Skipping cancelled item from queue: {download_id}")
394
+ processed_something = True
395
+ continue
396
+
397
+ # Update status to 'starting'
398
+ download_info["status"] = "starting"
399
+ download_info["start_time"] = datetime.datetime.now(datetime.timezone.utc).isoformat()
400
+ download_info["downloader_instance"] = None # Placeholder
401
+
402
+ # Add to active downloads BEFORE starting thread
403
+ self.active_downloads[download_id] = download_info
404
+
405
+ # Start download in a separate thread
406
+ thread = threading.Thread(
407
+ target=self._download_file_wrapper,
408
+ args=(download_info,),
409
+ daemon=True # Ensure thread exits if main program exits
410
+ )
411
+ thread.start()
412
+ print(f"[Manager] Starting download thread for: {download_info.get('filename', 'N/A')} ({download_id})")
413
+ processed_something = True
414
+
415
+ # Sleep only if nothing was processed to avoid busy-waiting
416
+ if not processed_something:
417
+ time.sleep(0.5) # Small delay before checking again
418
+
419
+ print("[Manager] Process queue thread stopped.")
420
+
421
+ # --- _update_download_status remains the same ---
422
+ def _update_download_status(self, download_id: str, status: Optional[str] = None,
423
+ progress: Optional[float] = None, speed: Optional[float] = None,
424
+ error: Optional[str] = None, connection_type: Optional[str] = None):
425
+ """Safely updates the status fields of an active download (thread-safe)."""
426
+ # ... (no changes needed here) ...
427
+ with self.lock:
428
+ if download_id in self.active_downloads:
429
+ item = self.active_downloads[download_id]
430
+ updated = False # Track if any field was actually updated
431
+ # Only update if value is provided
432
+ if status is not None and item.get("status") != status:
433
+ item["status"] = status
434
+ updated = True
435
+ # If status becomes terminal, record end time
436
+ if status in ["completed", "failed", "cancelled"] and not item.get("end_time"):
437
+ item["end_time"] = datetime.datetime.now(datetime.timezone.utc).isoformat()
438
+ if progress is not None:
439
+ # Clamp progress between 0 and 100
440
+ clamped_progress = max(0.0, min(100.0, progress))
441
+ # Only update if different enough to avoid excessive updates
442
+ if abs(item.get("progress", 0) - clamped_progress) > 0.01:
443
+ item["progress"] = clamped_progress
444
+ updated = True
445
+ if speed is not None:
446
+ clamped_speed = max(0.0, speed) # Speed cannot be negative
447
+ if item.get("speed") != clamped_speed: # Update if different
448
+ item["speed"] = clamped_speed
449
+ updated = True
450
+ if error is not None and item.get("error") != error:
451
+ # Update error only if it's new or different
452
+ item["error"] = str(error)[:500] # Limit length
453
+ updated = True
454
+ if connection_type is not None and connection_type != "N/A" and item.get("connection_type") != connection_type: # Only update if not N/A and different
455
+ item["connection_type"] = connection_type
456
+ updated = True
457
+
458
+ # --- _save_huggingface_metadata remains the same ---
459
+ def _save_huggingface_metadata(self, download_info: Dict[str, Any]):
460
+ """Saves the .cminfo.json file."""
461
+ # ... (no changes needed here) ...
462
+ output_path = download_info.get('output_path')
463
+ model_info = download_info.get('huggingface_model_info', {})
464
+ version_info = download_info.get('huggingface_version_info', {})
465
+ primary_file = download_info.get('huggingface_primary_file', {})
466
+ download_id = download_info.get('id', 'unknown')
467
+
468
+ try:
469
+ file_meta = primary_file.get('metadata', {}) or {} # Ensure dict
470
+ creator_info = model_info.get('creator', {}) or {}
471
+ model_stats = model_info.get('stats', {}) or {}
472
+ version_stats = version_info.get('stats', {}) or {}
473
+
474
+ metadata = {
475
+ "ModelId": model_info.get('id', version_info.get('modelId')) , # Use .get() on version info too
476
+ "ModelName": model_info.get('name', version_info.get('model',{}).get('name')), # Nested .get()
477
+ "ModelDescription": model_info.get('description'),
478
+ "CreatorUsername": creator_info.get('username'),
479
+ "Nsfw": model_info.get('nsfw', version_info.get('model',{}).get('nsfw')),
480
+ "Poi": model_info.get('poi', version_info.get('model',{}).get('poi')),
481
+ "AllowNoCredit": model_info.get('allowNoCredit', True),
482
+ "AllowCommercialUse": str(model_info.get('allowCommercialUse', 'Unknown')), # Ensure string
483
+ "AllowDerivatives": model_info.get('allowDerivatives', True),
484
+ "AllowDifferentLicense": model_info.get('allowDifferentLicense', True),
485
+ "Tags": model_info.get('tags', []),
486
+ "ModelType": model_info.get('type'),
487
+ "VersionId": version_info.get('id'),
488
+ "VersionName": version_info.get('name'),
489
+ "VersionDescription": version_info.get('description'),
490
+ "BaseModel": version_info.get('baseModel'),
491
+ "BaseModelType": version_info.get('baseModelType'),
492
+ "EarlyAccessDeadline": version_info.get('earlyAccessDeadline'),
493
+ "VersionPublishedAt": version_info.get('publishedAt'),
494
+ "VersionUpdatedAt": version_info.get('updatedAt'),
495
+ "VersionStatus": version_info.get('status'),
496
+ "IsPrimaryFile": primary_file.get('primary', False),
497
+ "PrimaryFileId": primary_file.get('id'),
498
+ "PrimaryFileName": primary_file.get('name'),
499
+ "FileMetadata": {
500
+ "fp": file_meta.get('fp'),
501
+ "size": file_meta.get('size'),
502
+ "format": file_meta.get('format', 'Unknown')
503
+ },
504
+ "ImportedAt": datetime.datetime.now(datetime.timezone.utc).isoformat(),
505
+ "Hashes": primary_file.get('hashes', {}),
506
+ "TrainedWords": version_info.get('trainedWords', []),
507
+ "Stats": {
508
+ "downloadCount": version_stats.get('downloadCount', model_stats.get('downloadCount', 0)),
509
+ "rating": version_stats.get('rating', model_stats.get('rating', 0)),
510
+ "ratingCount": version_stats.get('ratingCount', model_stats.get('ratingCount', 0)),
511
+ "favoriteCount": version_stats.get('favoriteCount', model_stats.get('favoriteCount', 0)), # Correct source needed? Check API docs
512
+ "commentCount": version_stats.get('commentCount', model_stats.get('commentCount', 0)), # Correct source needed? Check API docs
513
+ "thumbsUpCount": version_stats.get('thumbsUpCount', 0),
514
+ },
515
+ "DownloadUrlUsed": download_info.get('url'),
516
+ }
517
+
518
+ base, _ = os.path.splitext(output_path)
519
+ meta_filename = base + METADATA_SUFFIX
520
+ meta_path = os.path.join(os.path.dirname(output_path), meta_filename)
521
+
522
+ print(f"[Manager Meta {download_id}] Saving metadata to: {meta_path}")
523
+ with open(meta_path, 'w', encoding='utf-8') as f:
524
+ json.dump(metadata, f, indent=2, ensure_ascii=False)
525
+ print(f"[Manager Meta {download_id}] Metadata saved successfully.")
526
+
527
+ except Exception as e:
528
+ import traceback
529
+ print(f"[Manager Meta {download_id}] Error saving metadata file {meta_path}: {e}")
530
+ # traceback.print_exc() # Uncomment for full trace
531
+
532
+ # --- _download_and_save_preview remains the same ---
533
+ def _download_and_save_preview(self, download_info: Dict[str, Any]):
534
+ """Downloads and saves the .preview.jpeg file."""
535
+ # ... (no changes needed here) ...
536
+ output_path = download_info.get('output_path')
537
+ thumbnail_url = download_info.get('thumbnail')
538
+ api_key = download_info.get('api_key')
539
+ download_id = download_info.get('id', 'unknown')
540
+
541
+ if not output_path:
542
+ print(f"[Manager Preview {download_id}] Skipping preview download: Missing output path.")
543
+ return
544
+ if not thumbnail_url:
545
+ print(f"[Manager Preview {download_id}] Skipping preview download: No thumbnail URL provided.")
546
+ # Optionally try to find one in version_info images again? Might be redundant.
547
+ version_info = download_info.get('huggingface_version_info', {})
548
+ if version_info and isinstance(version_info.get('images'), list) and version_info['images']:
549
+ sorted_images = sorted([img for img in version_info['images'] if img and img.get("url")], key=lambda x: x.get('index', 0))
550
+ img_data = next((img for img in sorted_images if img.get("type") == "image" and "/width=" in img.get("url","")), None) # Prefer image type with width param
551
+ if not img_data: img_data = next((img for img in sorted_images if img.get("type") == "image"), None) # Fallback to any image type
552
+ if not img_data: img_data = next((img for img in sorted_images), None) # Fallback to any image at all
553
+ if img_data and img_data.get('url'):
554
+ thumbnail_url = img_data['url']
555
+ # Attempt to get a reasonable size (e.g. ~450px width)
556
+ if "/width=" in thumbnail_url:
557
+ thumbnail_url = thumbnail_url.split("/width=")[0] + "/width=450"
558
+ elif "/blob/" not in thumbnail_url: # Avoid adding params to blob URLs
559
+ separator = "&" if "?" in thumbnail_url else "?"
560
+ thumbnail_url += f"{separator}width=450"
561
+ print(f"[Manager Preview {download_id}] Found alternative thumbnail URL: {thumbnail_url}")
562
+ else:
563
+ print(f"[Manager Preview {download_id}] Still no thumbnail URL found in version info.")
564
+ return # Exit if still no URL
565
+ else:
566
+ return # Exit if no URL and no version info to search
567
+
568
+ base, _ = os.path.splitext(output_path)
569
+ preview_filename = base + PREVIEW_SUFFIX
570
+ preview_path = os.path.join(os.path.dirname(output_path), preview_filename)
571
+
572
+ print(f"[Manager Preview {download_id}] Downloading thumbnail from {thumbnail_url} to {preview_path}")
573
+ response = None
574
+ try:
575
+ headers = {}
576
+ if api_key: headers["Authorization"] = f"Bearer {api_key}"
577
+ response = requests.get(thumbnail_url, stream=True, headers=headers, timeout=METADATA_DOWNLOAD_TIMEOUT, allow_redirects=True)
578
+ response.raise_for_status()
579
+ content_type = response.headers.get('Content-Type', '').lower()
580
+ if not content_type.startswith('image/'):
581
+ print(f"[Manager Preview {download_id}] Warning: Thumbnail URL returned non-image content type '{content_type}'. Skipping save.")
582
+ return
583
+ with open(preview_path, 'wb') as f:
584
+ for chunk in response.iter_content(chunk_size=8192): f.write(chunk)
585
+ print(f"[Manager Preview {download_id}] Thumbnail downloaded successfully.")
586
+ except requests.exceptions.RequestException as e:
587
+ error_msg = f"Error downloading thumbnail {thumbnail_url}: {e}"
588
+ if hasattr(e, 'response') and e.response is not None: error_msg += f" (Status: {e.response.status_code})"
589
+ print(f"[Manager Preview {download_id}] {error_msg}")
590
+ except Exception as e: print(f"[Manager Preview {download_id}] Error saving thumbnail {preview_path}: {e}")
591
+ finally:
592
+ if response: response.close()
593
+
594
+ # --- _download_file_wrapper remains the same ---
595
+ def _download_file_wrapper(self, download_info: Dict[str, Any]):
596
+ """Wraps the download execution, handles status updates, exceptions, and metadata saving."""
597
+ # ... (no changes needed here) ...
598
+ download_id = download_info["id"]
599
+ filename = download_info.get('filename', download_id)
600
+ from .chunk_downloader import ChunkDownloader
601
+ downloader = None
602
+ success = False
603
+ final_status = "failed"
604
+ error_msg = None
605
+
606
+ try:
607
+ print(f"[Downloader Wrapper {download_id}] Preparing download for '{filename}'.")
608
+
609
+ # Handle case where url is None (repo downloads)
610
+ url = download_info["url"]
611
+ if url is None:
612
+ print(f"[Downloader Wrapper {download_id}] URL is None, using huggingface_hub")
613
+ # For repo downloads or single files, use huggingface_hub
614
+ from huggingface_hub import snapshot_download, hf_hub_download
615
+
616
+ # Extract just the repo_id from model_url_or_id (remove URL part)
617
+ model_url_or_id = download_info["model_url_or_id"]
618
+ if model_url_or_id.startswith("https://huggingface.co/"):
619
+ # Extract repo_id and filename from URL
620
+ parts = model_url_or_id.replace("https://huggingface.co/", "").split("/")
621
+ model_id = f"{parts[0]}/{parts[1]}" # Get "Kijai/WanVideo_comfy"
622
+
623
+ # Check if it's a specific file or entire repo
624
+ if len(parts) >= 4 and parts[2] == "resolve":
625
+ # It's a specific file: /resolve/commit/path/to/file
626
+ filename = "/".join(parts[4:]) # Get the file path after /resolve/commit/
627
+ print(f"[Downloader Wrapper {download_id}] Downloading single file {filename} from repo {model_id}")
628
+
629
+ # Use hf_hub_download for single file
630
+ result = hf_hub_download(
631
+ repo_id=model_id,
632
+ filename=filename,
633
+ local_dir=download_info["output_path"],
634
+ token=download_info.get("api_key")
635
+ )
636
+ else:
637
+ # It's an entire repo download
638
+ print(f"[Downloader Wrapper {download_id}] Downloading entire repo {model_id}")
639
+ result = snapshot_download(
640
+ repo_id=model_id,
641
+ local_dir=download_info["output_path"],
642
+ token=download_info.get("api_key")
643
+ )
644
+ else:
645
+ # Plain repo_id - assume entire repo download
646
+ model_id = model_url_or_id
647
+ print(f"[Downloader Wrapper {download_id}] Downloading entire repo {model_id}")
648
+ result = snapshot_download(
649
+ repo_id=model_id,
650
+ local_dir=download_info["output_path"],
651
+ token=download_info.get("api_key")
652
+ )
653
+
654
+ if result:
655
+ success = True
656
+ final_status = "completed"
657
+ print(f"[Downloader Wrapper {download_id}] Repo download completed: {result}")
658
+ else:
659
+ raise Exception("snapshot_download failed")
660
+ else:
661
+ # Normal file download
662
+ downloader = ChunkDownloader(
663
+ url=url,
664
+ output_path=download_info["output_path"],
665
+ num_connections=download_info.get("num_connections", DEFAULT_CONNECTIONS),
666
+ manager=self,
667
+ download_id=download_id,
668
+ api_key=download_info.get("api_key"),
669
+ known_size=download_info.get("known_size")
670
+ )
671
+
672
+ with self.lock:
673
+ if download_id not in self.active_downloads or self.active_downloads[download_id]["status"] == "cancelled":
674
+ print(f"[Downloader Wrapper {download_id}] Download was cancelled before instance could be fully linked/started.")
675
+ self._update_download_status(download_id, status="cancelled", error="Cancelled before start")
676
+ return
677
+
678
+ # Only set downloader_instance for file downloads
679
+ if url is not None:
680
+ self.active_downloads[download_id]["downloader_instance"] = downloader
681
+
682
+ self._update_download_status(download_id, status="downloading")
683
+ print(f"[Downloader Wrapper {download_id}] Starting download process for '{filename}'.")
684
+
685
+ # For repo downloads, success is already determined
686
+ if url is None:
687
+ # Repo download already completed above
688
+ print(f"[Downloader Wrapper {download_id}] Repo download already completed")
689
+ else:
690
+ # File download - use ChunkDownloader
691
+ success = downloader.download() # Blocking call
692
+ error_msg = downloader.error
693
+
694
+ if success:
695
+ final_status = "completed"
696
+ print(f"[Downloader Wrapper {download_id}] Download completed successfully for '{filename}'.")
697
+ try:
698
+ self._save_huggingface_metadata(download_info)
699
+ self._download_and_save_preview(download_info)
700
+ except Exception as meta_err:
701
+ print(f"[Downloader Wrapper {download_id}] Error during post-download metadata/preview saving: {meta_err}")
702
+
703
+ elif downloader.is_cancelled:
704
+ final_status = "cancelled"
705
+ error_msg = downloader.error or "Download cancelled"
706
+ print(f"[Downloader Wrapper {download_id}] Download cancelled for '{filename}'. Reason: {error_msg}")
707
+ else:
708
+ final_status = "failed"
709
+ error_msg = downloader.error or "Download failed with unknown error"
710
+ print(f"[Downloader Wrapper {download_id}] Download failed for '{filename}'. Error: {error_msg}")
711
+
712
+ except Exception as e:
713
+ import traceback
714
+ print(f"--- Critical Error in Download Wrapper {download_id} ('{filename}') ---")
715
+ traceback.print_exc()
716
+ print("--- End Error ---")
717
+ final_status = "failed"
718
+ error_msg = f"Unexpected wrapper error: {str(e)}"
719
+ if downloader and not downloader.is_cancelled:
720
+ try: downloader.cancel()
721
+ except: pass
722
+
723
+ finally:
724
+ final_progress_percent = 0
725
+ conn_type = download_info.get("connection_type", "N/A")
726
+
727
+ if downloader:
728
+ conn_type = downloader.connection_type
729
+ if downloader.total_size and downloader.total_size > 0:
730
+ final_progress_percent = (downloader.downloaded / downloader.total_size * 100)
731
+ if final_status == "completed": final_progress_percent = 100.0
732
+ final_progress_percent = min(100.0, max(0.0, final_progress_percent))
733
+
734
+ print(f"[Downloader Wrapper {download_id}] Finalizing status: {final_status}, Error: {error_msg}")
735
+ self._update_download_status(
736
+ download_id, status=final_status, progress=final_progress_percent,
737
+ speed=0, error=error_msg, connection_type=conn_type
738
+ )
739
+ if final_status == "completed":
740
+ print(f"[Manager] Download {download_id} completed ('{filename}'). Manual ComfyUI refresh may be needed for model list.")
741
+
742
+ # --- NEW: Retry Download Method ---
743
+ def retry_download(self, original_download_id: str) -> Dict[str, Any]:
744
+ """Finds a failed/cancelled download in history and re-queues it."""
745
+ with self.lock:
746
+ # Find the original download info in history
747
+ original_info = next((item for item in self.history if item.get("id") == original_download_id), None)
748
+
749
+ if not original_info:
750
+ return {"success": False, "error": f"Original download ID '{original_download_id}' not found in history."}
751
+
752
+ original_status = original_info.get("status")
753
+ if original_status not in ["failed", "cancelled"]:
754
+ return {"success": False, "error": f"Cannot retry download with status '{original_status}'. Only 'failed' or 'cancelled' are retryable."}
755
+
756
+ # --- Prepare the new download info dictionary ---
757
+ # Make a deep copy to avoid modifying the history item directly
758
+ try:
759
+ retry_info = json.loads(json.dumps(original_info))
760
+ print(retry_info)
761
+ except Exception as e:
762
+ return {"success": False, "error": f"Failed to copy original download data: {e}"}
763
+
764
+ # Remove fields specific to the *previous* attempt
765
+ retry_info.pop("id", None) # Will get a new ID
766
+ retry_info.pop("status", None)
767
+ retry_info.pop("progress", None)
768
+ retry_info.pop("speed", None)
769
+ retry_info.pop("error", None)
770
+ retry_info.pop("start_time", None)
771
+ retry_info.pop("end_time", None)
772
+ retry_info.pop("added_time", None)
773
+ retry_info.pop("connection_type", None)
774
+ retry_info.pop("downloader_instance", None)
775
+ # --- Crucially: Set force_redownload to True for retry ---
776
+ # This ensures it overwrites the potentially corrupted/partial file from the previous attempt.
777
+ retry_info["force_redownload"] = True
778
+
779
+ # --- Validate required fields for queuing (redundant check, but safe) ---
780
+ required_for_retry = [
781
+ 'url', 'output_path', 'num_connections', 'api_key', 'known_size',
782
+ 'huggingface_model_info', 'huggingface_version_info', 'huggingface_primary_file',
783
+ 'thumbnail', 'filename', 'model_url_or_id', 'model_version_id', 'model_type',
784
+ 'custom_filename', 'force_redownload'
785
+ ]
786
+ missing_keys = [key for key in required_for_retry if key not in retry_info or retry_info[key] is None and key != 'api_key' and key != 'custom_filename'] # Allow api_key/custom_filename to be None
787
+ #if missing_keys:
788
+ # return {"success": False, "error": f"Cannot retry: Original download data is missing required fields: {', '.join(missing_keys)}"}
789
+
790
+ # --- Add the prepared info to the queue (outside the lock for add_to_queue's own lock) ---
791
+ # Note: add_to_queue acquires its own lock internally
792
+ try:
793
+ new_download_id = self.add_to_queue(retry_info)
794
+ if new_download_id: # Check if add_to_queue returned a valid ID (indicating success)
795
+ with self.lock:
796
+ original_len = len(self.history)
797
+ # Filter out the item matching the original ID
798
+ self.history = [item for item in self.history if item.get("id") != original_download_id]
799
+ items_removed = original_len - len(self.history)
800
+
801
+ if items_removed == 1:
802
+ print(f"[Manager] Successfully removed original download '{original_download_id}' from history.")
803
+ return {
804
+ "success": True,
805
+ "message": f"Retry initiated. New download queued. Original removed from history.",
806
+ "new_download_id": new_download_id
807
+ }
808
+
809
+ else:
810
+ # Should have been caught by the except block, but as a failsafe
811
+ print(f"[Manager] Retry queueing failed for '{original_download_id}' for an unknown reason (no ID returned).")
812
+ return {"success": False, "error": "Failed to queue retry (unknown internal error)."}
813
+
814
+ except Exception as e:
815
+ print(f"[Manager] Error requeuing download for retry (Original ID: {original_download_id}): {e}")
816
+ return {"success": False, "error": f"Failed to queue retry: {e}"}
817
+
818
+ # --- NEW: Open Containing Folder Method ---
819
+ def open_containing_folder(self, download_id: str) -> Dict[str, Any]:
820
+ """Opens the directory containing the specified completed download file."""
821
+ file_path = None
822
+ with self.lock:
823
+ # Check history first (most likely location for completed items)
824
+ item_info = next((item for item in self.history if item.get("id") == download_id), None)
825
+ # Fallback to active (less likely, but possible if called very quickly after completion)
826
+ if not item_info and download_id in self.active_downloads:
827
+ item_info = self.active_downloads.get(download_id)
828
+
829
+ if not item_info:
830
+ return {"success": False, "error": "Download ID not found."}
831
+
832
+ if item_info.get("status") != "completed":
833
+ return {"success": False, "error": f"Cannot open path for download with status '{item_info.get('status')}'. Must be 'completed'."}
834
+
835
+ file_path = item_info.get("output_path") # Get the full path to the file
836
+
837
+ # --- Perform file operations outside the lock ---
838
+ if not file_path:
839
+ return {"success": False, "error": "Output path not found for this download."}
840
+
841
+ # --- Security Check: Ensure the path is within expected ComfyUI directories ---
842
+ # (This is a basic check, might need refinement based on your setup)
843
+ is_safe_path = False
844
+ try:
845
+ # Get the absolute path
846
+ abs_file_path = os.path.abspath(file_path)
847
+ folder_path = os.path.dirname(abs_file_path)
848
+
849
+ # Option 1: Check against ComfyUI's known directories (preferred)
850
+ if COMFY_PATHS_AVAILABLE:
851
+ # Check if the folder is within any known model type directory
852
+ known_types = [
853
+ "checkpoints", "loras", "vae", "embeddings", "hypernetworks",
854
+ "controlnet", "upscale_models", "clip_vision", "gligen", "configs",
855
+ "unet", "diffusers", "motion_models", "poses", "wildcards"
856
+ ]
857
+ known_dirs = [os.path.abspath(get_directory_by_type(t)) for t in known_types if get_directory_by_type(t)]
858
+ # Also allow output and input directories
859
+ if get_directory_by_type("output"): known_dirs.append(os.path.abspath(get_directory_by_type("output")))
860
+ if get_directory_by_type("input"): known_dirs.append(os.path.abspath(get_directory_by_type("input")))
861
+ # Add the plugin's own 'other_models' directory as safe
862
+ known_dirs.append(os.path.abspath(os.path.join(PLUGIN_ROOT, "other_models")))
863
+ # Add plugin-managed custom roots as safe
864
+ try:
865
+ import json as _json
866
+ _roots_file = os.path.join(PLUGIN_ROOT, "custom_roots.json")
867
+ if os.path.exists(_roots_file):
868
+ with open(_roots_file, 'r', encoding='utf-8') as _f:
869
+ _data = _json.load(_f)
870
+ if isinstance(_data, dict):
871
+ for _lst in _data.values():
872
+ if isinstance(_lst, list):
873
+ for _p in _lst:
874
+ if isinstance(_p, str):
875
+ known_dirs.append(os.path.abspath(_p))
876
+ except Exception as _e:
877
+ print(f"[Manager OpenPath] Warning: Failed to load custom roots: {_e}")
878
+ # Include all first-level subdirectories under models_dir as safe
879
+ try:
880
+ models_dir = getattr(__import__('folder_paths'), 'folder_paths').models_dir
881
+ except Exception:
882
+ models_dir = None
883
+ try:
884
+ if models_dir and os.path.isdir(models_dir):
885
+ for _name in os.listdir(models_dir):
886
+ _p = os.path.join(models_dir, _name)
887
+ if os.path.isdir(_p):
888
+ known_dirs.append(os.path.abspath(_p))
889
+ except Exception as _e2:
890
+ print(f"[Manager OpenPath] Warning: Failed enumerating models_dir subfolders: {_e2}")
891
+
892
+ for known_dir in known_dirs:
893
+ if os.path.commonpath([known_dir, folder_path]) == known_dir:
894
+ is_safe_path = True
895
+ break
896
+ else:
897
+ # Option 2: Fallback - Check if path is within the ComfyUI base directory
898
+ comfy_base = os.path.abspath(base_path)
899
+ if os.path.commonpath([comfy_base, folder_path]) == comfy_base:
900
+ is_safe_path = True
901
+ #print(f"[Manager OpenPath Warning] ComfyUI paths unavailable. Using base path check for {folder_path}")
902
+
903
+ if not is_safe_path:
904
+ print(f"[Manager OpenPath Denied] Path '{folder_path}' is outside known safe ComfyUI directories.")
905
+ return {"success": False, "error": "Cannot open path: Directory is outside allowed locations."}
906
+
907
+ # --- Open the directory ---
908
+ if not os.path.exists(folder_path):
909
+ return {"success": False, "error": f"Directory does not exist: {folder_path}"}
910
+ if not os.path.isdir(folder_path):
911
+ return {"success": False, "error": f"Path is not a directory: {folder_path}"}
912
+
913
+ try:
914
+ system = platform.system()
915
+ print(f"[Manager OpenPath] Attempting to open folder '{folder_path}' on {system}...")
916
+ if system == "Windows":
917
+ # Use startfile for better handling of spaces etc.
918
+ os.startfile(folder_path)
919
+ elif system == "Darwin": # macOS
920
+ subprocess.check_call(["open", folder_path])
921
+ elif system == "Linux":
922
+ # Use xdg-open, handle potential errors if command not found
923
+ try:
924
+ subprocess.check_call(["xdg-open", folder_path])
925
+ except FileNotFoundError:
926
+ # Fallback for headless systems or if xdg-open isn't installed
927
+ print(f"[Manager OpenPath] 'xdg-open' not found. Cannot automatically open folder on this Linux system.")
928
+ return {"success": False, "error": "'xdg-open' command not found. Cannot open folder."}
929
+ else:
930
+ print(f"[Manager OpenPath] Unsupported operating system: {system}. Cannot open folder.")
931
+ return {"success": False, "error": f"Unsupported OS ({system}) for opening folder."}
932
+
933
+ print(f"[Manager OpenPath] Successfully requested folder opening for '{folder_path}'.")
934
+ return {"success": True, "message": f"Opened directory: {folder_path}"}
935
+
936
+ except Exception as e:
937
+ print(f"[Manager OpenPath] Failed to open directory '{folder_path}': {e}")
938
+ return {"success": False, "error": f"Failed to open directory: {e}"}
939
+
940
+ except Exception as path_err:
941
+ # Catch errors during path validation itself
942
+ print(f"[Manager OpenPath] Error during path validation/retrieval for {download_id}: {path_err}")
943
+ return {"success": False, "error": f"Error processing path: {path_err}"}
944
+
945
+ # --- Global Instance ---
946
+ manager = DownloadManager(max_concurrent=MAX_CONCURRENT_DOWNLOADS)
947
+
948
+ # --- Graceful Shutdown ---
949
+ # (shutdown_manager remains the same)
950
+ def shutdown_manager():
951
+ # ... (no changes) ...
952
+ print("[Manager] Shutdown requested.")
953
+ if manager:
954
+ manager.running = False
955
+ acquired_lock = False
956
+ try: acquired_lock = manager.lock.acquire(timeout=1.0)
957
+ except RuntimeError: pass # Lock might not be initialised if init failed
958
+
959
+ if acquired_lock:
960
+ try:
961
+ active_ids = list(manager.active_downloads.keys())
962
+ queue_ids = [item['id'] for item in manager.queue]
963
+ print(f"[Manager] Requesting cancellation for {len(active_ids)} active and {len(queue_ids)} queued downloads on shutdown...")
964
+ all_ids_to_cancel = active_ids + queue_ids
965
+ manager.lock.release() # Release lock BEFORE calling cancel_download
966
+
967
+ for dl_id in all_ids_to_cancel:
968
+ try:
969
+ # Reuse cancel_download which handles both active and queued safely
970
+ manager.cancel_download(dl_id)
971
+ except Exception as e:
972
+ print(f"Error cancelling {dl_id} during shutdown: {e}")
973
+ except Exception as e:
974
+ print(f"[Manager] Error accessing lists during shutdown (after lock acquired): {e}")
975
+ try: manager.lock.release() # Ensure release on error
976
+ except RuntimeError: pass
977
+ # Give threads/tasks a brief moment to react
978
+ time.sleep(0.5)
979
+ else:
980
+ print("[Manager] Warning: Could not acquire lock to cancel downloads during shutdown.")
981
+
982
+ # Attempt to join the manager's process thread (best effort)
983
+ try:
984
+ if manager._process_thread and manager._process_thread.is_alive():
985
+ manager._process_thread.join(timeout=2.0)
986
+ if manager._process_thread.is_alive():
987
+ print("[Manager] Warning: Process thread did not exit cleanly within timeout.")
988
+ except Exception as e:
989
+ print(f"[Manager] Error joining manager thread during shutdown: {e}")
990
+ print("[Manager] Shutdown complete.")
991
+
992
+ import atexit
993
+ atexit.register(shutdown_manager)
994
+
995
+ # Create the global manager instance
996
+ manager = DownloadManager()
custom_nodes/ComfyUI-HuggingFace/server/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ # ================================================
2
+ # File: server/__init__.py
3
+ # ================================================
4
+ # Import the routes package to ensure all route decorators are executed.
5
+ from . import routes
custom_nodes/ComfyUI-HuggingFace/server/routes/CancelDownload.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ================================================
2
+ # File: server/routes/CancelDownload.py
3
+ # ================================================
4
+ import json
5
+ from aiohttp import web
6
+ import server # ComfyUI server instance
7
+ from ..utils import get_request_json
8
+ from ...downloader.manager import manager as download_manager
9
+
10
+ prompt_server = server.PromptServer.instance
11
+
12
+ @prompt_server.routes.post("/api/huggingface/cancel")
13
+ async def route_cancel_download(request):
14
+ """API Endpoint to cancel a download."""
15
+ try:
16
+ data = await get_request_json(request)
17
+ download_id = data.get("download_id")
18
+ if not download_id:
19
+ print("not download id " + download_id)
20
+ raise web.HTTPBadRequest(reason="Missing 'download_id'")
21
+
22
+ success = download_manager.cancel_download(download_id)
23
+ if success:
24
+ return web.json_response({
25
+ "status": "cancelled", # Or "cancellation_requested" ?
26
+ "message": f"Cancellation requested for download ID: {download_id}.",
27
+ "download_id": download_id
28
+ })
29
+ else:
30
+ # Might be already completed/failed/cancelled and in history, or invalid ID
31
+ raise web.HTTPNotFound(reason=f"Download ID {download_id} not found in active queue or running downloads.")
32
+
33
+ except web.HTTPError as http_err:
34
+ # Consistent error handling
35
+ body_detail = ""
36
+ try:
37
+ body_detail = await http_err.text() if hasattr(http_err, 'text') else http_err.body.decode('utf-8', errors='ignore') if http_err.body else ""
38
+ if body_detail.startswith('{') and body_detail.endswith('}'): body_detail = json.loads(body_detail)
39
+ except Exception: pass
40
+ return web.json_response({"error": http_err.reason, "details": body_detail or "No details", "status_code": http_err.status}, status=http_err.status)
41
+
42
+ except Exception as e:
43
+ print(f"Error cancelling download: {e}")
44
+ return web.json_response({"error": "Internal Server Error", "details": f"Failed to cancel download: {str(e)}", "status_code": 500}, status=500)
custom_nodes/ComfyUI-HuggingFace/server/routes/ClearHistory.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ================================================
2
+ # File: server/routes/ClearHistory.py
3
+ # ================================================
4
+ import asyncio
5
+ from aiohttp import web
6
+
7
+ import server # ComfyUI server instance
8
+ from ...downloader.manager import manager as download_manager
9
+
10
+ prompt_server = server.PromptServer.instance
11
+
12
+ @prompt_server.routes.post("/api/huggingface/clear_history")
13
+ async def route_clear_history(request):
14
+ """API Endpoint to clear the download history."""
15
+ if not download_manager:
16
+ return web.json_response({"error": "Download Manager not initialized"}, status=500)
17
+
18
+ try:
19
+ # No request body needed for this action
20
+ print(f"[API Route /huggingface/clear_history] Received clear history request.")
21
+
22
+ # Call manager method in thread
23
+ result = await asyncio.to_thread(download_manager.clear_history)
24
+
25
+ status_code = 200 if result.get("success") else 500 # Use 500 for internal clear error
26
+ return web.json_response(result, status=status_code)
27
+
28
+ except Exception as e:
29
+ import traceback
30
+ print(f"Error handling /huggingface/clear_history request: {e}")
31
+ # traceback.print_exc() # Uncomment for detailed logs
32
+ return web.json_response({"error": "Internal Server Error", "details": f"An unexpected error occurred: {str(e)}"}, status=500)
custom_nodes/ComfyUI-HuggingFace/server/routes/DownloadModel.py ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ================================================
2
+ # File: server/routes/DownloadModel.py
3
+ # ================================================
4
+ import os
5
+ import json
6
+ import traceback
7
+ import re
8
+ from aiohttp import web
9
+
10
+ import server # ComfyUI server instance
11
+ from ..utils import get_request_json, resolve_huggingface_api_key
12
+ from ...downloader.manager import manager as download_manager
13
+ from ...api.huggingface import HuggingFaceAPI
14
+ from ...utils.helpers import get_model_dir, parse_huggingface_input, sanitize_filename
15
+ from ...config import METADATA_SUFFIX, PREVIEW_SUFFIX
16
+
17
+ prompt_server = server.PromptServer.instance
18
+
19
+ @prompt_server.routes.post("/api/huggingface/download")
20
+ async def route_download_model(request):
21
+ """API Endpoint to initiate a download."""
22
+ try:
23
+ data = await get_request_json(request)
24
+
25
+ model_url_or_id = data.get("model_url_or_id")
26
+ model_type_value = data.get("model_type", "checkpoint")
27
+ explicit_save_root = (data.get("save_root") or "").strip()
28
+ custom_filename_input = data.get("custom_filename", "").strip()
29
+ selected_subdir = (data.get("subdir") or "").strip()
30
+ num_connections = int(data.get("num_connections", 4))
31
+ force_redownload = bool(data.get("force_redownload", False))
32
+ resolved_api_key = resolve_huggingface_api_key(data)
33
+
34
+ if not model_url_or_id:
35
+ raise web.HTTPBadRequest(reason="Missing 'model_url_or_id'")
36
+
37
+ print(f"[HF Download] Request: {model_url_or_id}, SaveType: {model_type_value}")
38
+
39
+ # Parse HuggingFace URL/ID
40
+ parsed_model_id, parsed_filename = parse_huggingface_input(model_url_or_id)
41
+
42
+ if not parsed_model_id:
43
+ raise web.HTTPBadRequest(reason=f"Could not parse HuggingFace model ID from: {model_url_or_id}")
44
+
45
+ target_model_id = parsed_model_id
46
+ print(f"[HF Download] Parsed Model ID: {target_model_id}")
47
+
48
+ # Initialize API
49
+ api = HuggingFaceAPI(resolved_api_key)
50
+
51
+ if parsed_filename:
52
+ # Direct download from URL - skip API calls
53
+ target_filename = parsed_filename
54
+ # Try to get model name from the model_id itself
55
+ model_name = target_model_id.split('/')[-1] if target_model_id else "Unknown Model"
56
+ model_info = {"id": target_model_id, "name": model_name}
57
+ print(f"[HF Download] Direct download file: {target_filename}")
58
+ print(f"[HF Download] Using extracted model name: {model_name}")
59
+ else:
60
+ # Skip API calls for public repos, use only huggingface_hub
61
+ if resolved_api_key:
62
+ # For private repos, try API calls to get model info
63
+ api = HuggingFaceAPI(resolved_api_key)
64
+ model_info = api.get_model_info(target_model_id)
65
+
66
+ if not model_info or "error" in model_info:
67
+ print(f"[HF Download] Model info failed, using huggingface_hub directly")
68
+ target_filename = parsed_filename if parsed_filename else None
69
+ model_info = {"id": target_model_id, "name": target_model_id.split('/')[-1]}
70
+ else:
71
+ # For private repos, still use huggingface_hub for download
72
+ target_filename = parsed_filename if parsed_filename else None
73
+ else:
74
+ # For public repos, skip API calls entirely
75
+ print(f"[HF Download] Public repo, using huggingface_hub directly")
76
+ target_filename = parsed_filename if parsed_filename else None
77
+ model_info = {"id": target_model_id, "name": target_model_id.split('/')[-1]}
78
+
79
+ if not target_filename:
80
+ print(f"[HF Download] No specific file found, letting huggingface_hub auto-detect")
81
+ target_filename = None
82
+
83
+ print(f"[HF Download] Target file: {target_filename}")
84
+
85
+ # Determine save directory
86
+ target_dir = get_model_dir(model_type_value, explicit_save_root, selected_subdir)
87
+ if not target_dir:
88
+ raise web.HTTPBadRequest(reason=f"Invalid model type: {model_type_value}")
89
+
90
+ # Determine filename
91
+ if custom_filename_input:
92
+ final_filename = sanitize_filename(custom_filename_input)
93
+ elif target_filename is None:
94
+ # For repo downloads, use model name as folder
95
+ final_filename = model_info.get("name", target_model_id.split('/')[-1])
96
+ else:
97
+ final_filename = os.path.basename(target_filename)
98
+
99
+ save_path = os.path.join(target_dir, final_filename)
100
+
101
+ # Check if file exists
102
+ if os.path.exists(save_path) and not force_redownload:
103
+ raise web.HTTPBadRequest(reason=f"File already exists: {final_filename}")
104
+
105
+ # Start download
106
+ if target_filename is None:
107
+ # For repo downloads, don't construct URL - let huggingface_hub handle it
108
+ download_url = None
109
+ else:
110
+ download_url = f"https://huggingface.co/{target_model_id}/resolve/main/{target_filename}"
111
+
112
+ download_info = {
113
+ "model_url_or_id": model_url_or_id,
114
+ "save_path": save_path,
115
+ "output_path": save_path, # Add this for ChunkDownloader
116
+ "url": download_url, # Add this for ChunkDownloader
117
+ "filename": final_filename,
118
+ "model_type": model_type_value,
119
+ "download_url": download_url,
120
+ "huggingface_model_info": model_info,
121
+ "huggingface_filename": target_filename,
122
+ "num_connections": num_connections,
123
+ "force_redownload": force_redownload,
124
+ # Add missing fields to prevent warnings
125
+ "api_key": resolved_api_key,
126
+ "known_size": None,
127
+ "huggingface_version_info": {},
128
+ "huggingface_primary_file": None,
129
+ "thumbnail": None,
130
+ "model_version_id": None,
131
+ "custom_filename": custom_filename_input,
132
+ "huggingface_model_name": model_info.get("name", target_model_id.split('/')[-1])
133
+ }
134
+
135
+ download_id = download_manager.add_to_queue(download_info)
136
+
137
+ # Extract model name from model_info if available, otherwise from model_id
138
+ print(f"[DEBUG] model_info: {model_info}")
139
+ print(f"[DEBUG] target_model_id: {target_model_id}")
140
+
141
+ # Try to parse model_info as JSON if it's a string
142
+ parsed_model_info = None
143
+ if isinstance(model_info, str):
144
+ try:
145
+ import json
146
+ parsed_model_info = json.loads(model_info)
147
+ print(f"[DEBUG] Parsed model_info from JSON: {parsed_model_info}")
148
+ except:
149
+ print(f"[DEBUG] Failed to parse model_info as JSON")
150
+ parsed_model_info = None
151
+ else:
152
+ parsed_model_info = model_info
153
+
154
+ if parsed_model_info and isinstance(parsed_model_info, dict) and parsed_model_info.get('name'):
155
+ model_display_name = parsed_model_info['name']
156
+ print(f"[DEBUG] Using parsed model_info name: {model_display_name}")
157
+ elif model_info and isinstance(model_info, dict) and model_info.get('name'):
158
+ model_display_name = model_info['name']
159
+ print(f"[DEBUG] Using model_info name: {model_display_name}")
160
+ else:
161
+ model_display_name = target_model_id.split('/')[-1] if target_model_id else "Unknown Model"
162
+ print(f"[DEBUG] Using parsed name: {model_display_name}")
163
+
164
+ print(f"[DEBUG] Final model_display_name: {model_display_name}")
165
+
166
+ response_data = {
167
+ "download_id": download_id,
168
+ "huggingface_model_id": target_model_id,
169
+ "huggingface_model_name": model_display_name, # Add model name
170
+ "huggingface_filename": target_filename,
171
+ "huggingface_model_info": model_info,
172
+ "save_path": save_path,
173
+ "filename": final_filename,
174
+ "status": "queued" # Changed from "started" to "queued" to match frontend expectation
175
+ }
176
+
177
+ print(f"[DEBUG] Response data huggingface_model_name: {response_data['huggingface_model_name']}")
178
+
179
+ return web.json_response(response_data)
180
+
181
+ except web.HTTPException:
182
+ raise
183
+ except Exception as e:
184
+ print(f"--- Unhandled Error in /huggingface/download ---")
185
+ traceback.print_exc()
186
+ raise web.HTTPInternalServerError(reason=str(e))
custom_nodes/ComfyUI-HuggingFace/server/routes/GetBaseModels.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ================================================
2
+ # File: server/routes/GetBaseModels.py
3
+ # ================================================
4
+ from aiohttp import web
5
+ import yaml
6
+ import re
7
+ import server # ComfyUI server instance
8
+ from ...config import AVAILABLE_MEILI_BASE_MODELS
9
+ from ...api.huggingface import HuggingFaceAPI
10
+ from ..utils import resolve_huggingface_api_key
11
+
12
+ prompt_server = server.PromptServer.instance
13
+
14
+ @prompt_server.routes.get("/api/huggingface/base_models")
15
+ async def route_get_base_models(request):
16
+ """API Endpoint to get the known base model types for filtering."""
17
+ try:
18
+ # Try to get base models from HuggingFace API
19
+ api_key = resolve_huggingface_api_key({})
20
+ api = HuggingFaceAPI(api_key)
21
+
22
+ # Get popular models and extract their base models
23
+ popular_models = [
24
+ "stabilityai/stable-diffusion-xl-base-1.0",
25
+ "runwayml/stable-diffusion-v1-5",
26
+ "stabilityai/stable-diffusion-2-1",
27
+ "CompVis/stable-diffusion-v1-4",
28
+ "black-forest-labs/FLUX.1-dev",
29
+ "black-forest-labs/FLUX.1-schnell"
30
+ ]
31
+
32
+ base_models = set()
33
+ base_models.update(AVAILABLE_MEILI_BASE_MODELS) # Keep hardcoded ones as fallback
34
+
35
+ for model_id in popular_models:
36
+ try:
37
+ model_info = api.get_model_info(model_id)
38
+ if model_info and not isinstance(model_info, dict) or "error" not in model_info:
39
+ # Add the model itself as a base model
40
+ base_models.add(model_id)
41
+ except Exception as e:
42
+ print(f"[GetBaseModels] Error getting info for {model_id}: {e}")
43
+ continue
44
+
45
+ return web.json_response({"base_models": sorted(list(base_models))})
46
+
47
+ except Exception as e:
48
+ print(f"Error getting base model types: {e}")
49
+ # Fallback to hardcoded list
50
+ return web.json_response({"base_models": AVAILABLE_MEILI_BASE_MODELS})
51
+ return web.json_response({"error": "Internal Server Error", "details": str(e), "status_code": 500}, status=500)
custom_nodes/ComfyUI-HuggingFace/server/routes/GetModelDetails.py ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ================================================
2
+ # File: server/routes/GetModelDetails.py
3
+ # ================================================
4
+ import os
5
+ import json
6
+ import traceback
7
+ from aiohttp import web
8
+
9
+ import server # ComfyUI server instance
10
+ from ..utils import get_request_json, resolve_huggingface_api_key
11
+ from ...api.huggingface import HuggingFaceAPI
12
+
13
+ prompt_server = server.PromptServer.instance
14
+
15
+ @prompt_server.routes.post("/api/huggingface/get_model_details")
16
+ async def route_get_model_details(request):
17
+ """API Endpoint to fetch model details from HuggingFace model card."""
18
+ try:
19
+ data = await get_request_json(request)
20
+ model_url_or_id = data.get("model_url_or_id")
21
+ resolved_api_key = resolve_huggingface_api_key(data)
22
+
23
+ if not model_url_or_id:
24
+ raise web.HTTPBadRequest(reason="Missing 'model_url_or_id'")
25
+
26
+ # Parse model ID
27
+ from ...utils.helpers import parse_huggingface_input
28
+ parsed_model_id, parsed_filename = parse_huggingface_input(model_url_or_id)
29
+
30
+ if not parsed_model_id:
31
+ raise web.HTTPBadRequest(reason=f"Could not parse HuggingFace model ID from: {model_url_or_id}")
32
+
33
+ # Get model info using huggingface_hub
34
+ try:
35
+ from huggingface_hub import ModelCard
36
+ model_card = ModelCard.load(parsed_model_id, token=resolved_api_key)
37
+
38
+ # Extract data from model card
39
+ model_name = model_card.data.get("model_name", parsed_model_id.split('/')[-1])
40
+ creator_username = model_card.data.get("author", "Unknown Creator")
41
+ description = model_card.text
42
+ base_model = model_card.data.get("base_model", [])
43
+ tags = model_card.data.get("tags", [])
44
+ license_info = model_card.data.get("license", "Unknown")
45
+
46
+ # Get model info from API as fallback for stats
47
+ try:
48
+ api = HuggingFaceAPI(resolved_api_key)
49
+ api_info = api.get_model_info(parsed_model_id)
50
+
51
+ stats = {}
52
+ if api_info and not isinstance(api_info, dict) or "error" not in api_info:
53
+ stats = {
54
+ "downloads": api_info.get("downloads", 0),
55
+ "likes": api_info.get("likes", 0),
56
+ "created_at": api_info.get("created_at", ""),
57
+ "modified_at": api_info.get("modified_at", "")
58
+ }
59
+ except Exception as api_error:
60
+ print(f"[GetModelDetails] API info failed, using model card only: {api_error}")
61
+ stats = {}
62
+
63
+ response_data = {
64
+ "model_name": model_name,
65
+ "creator_username": creator_username,
66
+ "description": description,
67
+ "base_model": base_model,
68
+ "tags": tags,
69
+ "license": license_info,
70
+ "stats": stats,
71
+ "model_id": parsed_model_id,
72
+ "huggingface_model_name": model_name
73
+ }
74
+
75
+ return web.json_response(response_data)
76
+
77
+ except Exception as e:
78
+ print(f"[GetModelDetails] Error loading model card: {e}")
79
+ # Try to get at least basic info from API
80
+ try:
81
+ api = HuggingFaceAPI(resolved_api_key)
82
+ api_info = api.get_model_info(parsed_model_id)
83
+
84
+ if api_info and not isinstance(api_info, dict) or "error" not in api_info:
85
+ return web.json_response({
86
+ "model_name": api_info.get("id", parsed_model_id.split('/')[-1]),
87
+ "creator_username": api_info.get("author", "Unknown Creator"),
88
+ "description": api_info.get("description", "No description available"),
89
+ "base_model": [],
90
+ "tags": api_info.get("tags", []),
91
+ "license": api_info.get("license", "Unknown"),
92
+ "stats": {
93
+ "downloads": api_info.get("downloads", 0),
94
+ "likes": api_info.get("likes", 0),
95
+ "created_at": api_info.get("created_at", ""),
96
+ "modified_at": api_info.get("modified_at", "")
97
+ },
98
+ "model_id": parsed_model_id,
99
+ "huggingface_model_name": api_info.get("id", parsed_model_id.split('/')[-1])
100
+ })
101
+ except Exception as api_error:
102
+ print(f"[GetModelDetails] API fallback also failed: {api_error}")
103
+
104
+ # Final fallback to minimal info
105
+ return web.json_response({
106
+ "model_name": parsed_model_id.split('/')[-1],
107
+ "creator_username": "Unknown",
108
+ "description": "Model details not available",
109
+ "base_model": [],
110
+ "tags": [],
111
+ "license": "Unknown",
112
+ "stats": {},
113
+ "model_id": parsed_model_id,
114
+ "huggingface_model_name": parsed_model_id.split('/')[-1]
115
+ })
116
+
117
+ except Exception as e:
118
+ print(f"Error in get_model_details: {e}")
119
+ traceback.print_exc()
120
+ return web.json_response({"error": "Internal Server Error", "details": str(e)}, status=500)
custom_nodes/ComfyUI-HuggingFace/server/routes/GetModelDirs.py ADDED
@@ -0,0 +1,337 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ================================================
2
+ # File: server/routes/GetModelDirs.py
3
+ # ================================================
4
+ import os
5
+ import json
6
+ from aiohttp import web
7
+
8
+ import server # ComfyUI server instance
9
+ from ...utils.helpers import (
10
+ get_model_dir,
11
+ get_model_folder_paths,
12
+ get_model_type_folder_name,
13
+ sanitize_filename,
14
+ )
15
+ from ...config import PLUGIN_ROOT
16
+ import folder_paths
17
+
18
+ prompt_server = server.PromptServer.instance
19
+
20
+ CUSTOM_ROOTS_FILE = os.path.join(PLUGIN_ROOT, "custom_roots.json")
21
+ ROOT_SETTINGS_FILE = os.path.join(PLUGIN_ROOT, "root_settings.json")
22
+ GLOBAL_ROOT_KEY = "global_default_root"
23
+
24
+ def _load_custom_roots():
25
+ try:
26
+ if os.path.exists(CUSTOM_ROOTS_FILE):
27
+ with open(CUSTOM_ROOTS_FILE, 'r', encoding='utf-8') as f:
28
+ data = json.load(f)
29
+ if isinstance(data, dict):
30
+ # Normalize values to lists of strings
31
+ return {k: [str(p) for p in (v or []) if isinstance(p, str)] for k, v in data.items()}
32
+ except Exception as e:
33
+ print(f"[HuggingFace] Warning: Failed to load custom roots: {e}")
34
+ return {}
35
+
36
+ def _save_custom_roots(data):
37
+ try:
38
+ with open(CUSTOM_ROOTS_FILE, 'w', encoding='utf-8') as f:
39
+ json.dump(data, f, indent=2)
40
+ return True
41
+ except Exception as e:
42
+ print(f"[HuggingFace] Error writing custom roots file: {e}")
43
+ return False
44
+
45
+ def _load_root_settings():
46
+ try:
47
+ if os.path.exists(ROOT_SETTINGS_FILE):
48
+ with open(ROOT_SETTINGS_FILE, 'r', encoding='utf-8') as f:
49
+ data = json.load(f)
50
+ if isinstance(data, dict):
51
+ return data
52
+ except Exception as e:
53
+ print(f"[HuggingFace] Warning: Failed to load root settings: {e}")
54
+ return {}
55
+
56
+ def _save_root_settings(data):
57
+ try:
58
+ with open(ROOT_SETTINGS_FILE, 'w', encoding='utf-8') as f:
59
+ json.dump(data, f, indent=2)
60
+ return True
61
+ except Exception as e:
62
+ print(f"[HuggingFace] Error writing root settings file: {e}")
63
+ return False
64
+
65
+ def get_global_default_root():
66
+ settings = _load_root_settings()
67
+ raw = settings.get(GLOBAL_ROOT_KEY)
68
+ if isinstance(raw, str):
69
+ raw = raw.strip()
70
+ if raw:
71
+ return os.path.abspath(raw)
72
+ return None
73
+
74
+ def _set_global_default_root(path):
75
+ settings = _load_root_settings()
76
+ if path and str(path).strip():
77
+ settings[GLOBAL_ROOT_KEY] = os.path.abspath(str(path).strip())
78
+ else:
79
+ settings.pop(GLOBAL_ROOT_KEY, None)
80
+ return _save_root_settings(settings)
81
+
82
+ def _get_global_root_for_type(model_type: str):
83
+ global_root = get_global_default_root()
84
+ if not global_root:
85
+ return None
86
+ model_subfolder = get_model_type_folder_name(model_type)
87
+ return os.path.abspath(os.path.join(global_root, model_subfolder))
88
+
89
+ def _get_effective_base_dir(model_type: str):
90
+ global_type_root = _get_global_root_for_type(model_type)
91
+ if global_type_root:
92
+ return global_type_root
93
+ return get_model_dir(model_type)
94
+
95
+ def _get_custom_roots_for_type(model_type: str):
96
+ roots_map = _load_custom_roots()
97
+ model_type_raw = (model_type or '').lower().strip()
98
+ canonical_type = get_model_type_folder_name(model_type_raw)
99
+ custom = []
100
+ for key in [model_type_raw, canonical_type]:
101
+ for p in roots_map.get(key, []):
102
+ ap = os.path.abspath(p)
103
+ if ap not in custom:
104
+ custom.append(ap)
105
+ return custom
106
+
107
+ def _get_all_roots_for_type(model_type: str):
108
+ model_type = (model_type or '').lower().strip()
109
+ roots = []
110
+
111
+ # Prefer global default root for this type when configured
112
+ global_type_root = _get_global_root_for_type(model_type)
113
+ if global_type_root and global_type_root not in roots:
114
+ roots.append(global_type_root)
115
+
116
+ # Include all registered paths from ComfyUI (respects extra_model_paths.yaml)
117
+ for p in get_model_folder_paths(model_type):
118
+ if p not in roots:
119
+ roots.append(p)
120
+
121
+ # Include plugin custom roots
122
+ for p in _get_custom_roots_for_type(model_type):
123
+ if p not in roots:
124
+ roots.append(p)
125
+
126
+ # Ensure at least one sane fallback
127
+ if not roots:
128
+ d = get_model_dir(model_type)
129
+ if d:
130
+ roots.append(os.path.abspath(d))
131
+
132
+ # Include all immediate subdirectories inside the main ComfyUI models folder
133
+ try:
134
+ models_dir = getattr(folder_paths, 'models_dir', None)
135
+ if not models_dir:
136
+ base = getattr(folder_paths, 'base_path', os.getcwd())
137
+ models_dir = os.path.join(base, 'models')
138
+ if os.path.isdir(models_dir):
139
+ for name in os.listdir(models_dir):
140
+ p = os.path.join(models_dir, name)
141
+ if os.path.isdir(p):
142
+ ap = os.path.abspath(p)
143
+ if ap not in roots:
144
+ roots.append(ap)
145
+ except Exception as e:
146
+ print(f"[HuggingFace] Warning: Failed to enumerate models dir subfolders: {e}")
147
+ return roots
148
+
149
+ def _list_subdirs(root_dir: str, max_entries: int = 5000):
150
+ """Return a sorted list of relative subdirectory paths under root_dir, including nested."""
151
+ rel_dirs = set()
152
+ root_dir = os.path.abspath(root_dir)
153
+ count = 0
154
+ for current, dirs, _files in os.walk(root_dir):
155
+ # Avoid following symlinks to reduce risk
156
+ abs_current = os.path.abspath(current)
157
+ try:
158
+ rel = os.path.relpath(abs_current, root_dir)
159
+ except Exception:
160
+ continue
161
+ if rel == ".":
162
+ rel = "" # represent root as empty
163
+ rel_dirs.add(rel)
164
+ count += 1
165
+ if count >= max_entries:
166
+ break
167
+ return sorted(rel_dirs)
168
+
169
+ @prompt_server.routes.get("/api/huggingface/model_dirs")
170
+ async def route_get_model_dirs(request):
171
+ """List the base directory (or provided root) and all subdirectories for a given model type."""
172
+ model_type = request.query.get("type", "checkpoints").lower().strip()
173
+ root = (request.query.get("root") or "").strip()
174
+ try:
175
+ base_dir = os.path.abspath(root) if root else _get_effective_base_dir(model_type)
176
+ os.makedirs(base_dir, exist_ok=True)
177
+ subdirs = _list_subdirs(base_dir)
178
+ global_root = get_global_default_root()
179
+ return web.json_response({
180
+ "model_type": model_type,
181
+ "base_dir": base_dir,
182
+ "subdirs": subdirs, # relative paths, "" represents the base root
183
+ "global_root": global_root or "",
184
+ "using_global_root": bool(global_root and not root),
185
+ })
186
+ except Exception as e:
187
+ return web.json_response({"error": "Failed to list directories", "details": str(e)}, status=500)
188
+
189
+ @prompt_server.routes.post("/api/huggingface/create_dir")
190
+ async def route_create_model_dir(request):
191
+ """Create a new subdirectory under a model type's base directory."""
192
+ try:
193
+ data = await request.json()
194
+ model_type = (data.get("model_type") or "checkpoints").lower().strip()
195
+ new_dir = (data.get("new_dir") or "").strip()
196
+ if not new_dir:
197
+ return web.json_response({"error": "Missing 'new_dir'"}, status=400)
198
+
199
+ # If client provided an explicit root, prefer it
200
+ base_dir = (data.get("root") or "").strip()
201
+ base_dir = os.path.abspath(base_dir) if base_dir else _get_effective_base_dir(model_type)
202
+ os.makedirs(base_dir, exist_ok=True)
203
+
204
+ # Normalize and sanitize each part; disallow absolute and traversal
205
+ norm = os.path.normpath(new_dir.replace("\\", "/"))
206
+ parts = [p for p in norm.split("/") if p and p not in (".", "..")]
207
+ safe_parts = [sanitize_filename(p) for p in parts]
208
+ rel_path = os.path.join(*safe_parts) if safe_parts else ""
209
+ if not rel_path:
210
+ return web.json_response({"error": "Invalid folder name"}, status=400)
211
+
212
+ abs_path = os.path.abspath(os.path.join(base_dir, rel_path))
213
+ # Ensure it remains inside base_dir
214
+ if os.path.commonpath([abs_path, os.path.abspath(base_dir)]) != os.path.abspath(base_dir):
215
+ return web.json_response({"error": "Invalid path"}, status=400)
216
+
217
+ os.makedirs(abs_path, exist_ok=True)
218
+ return web.json_response({
219
+ "success": True,
220
+ "created": rel_path,
221
+ "abs_path": abs_path,
222
+ })
223
+ except Exception as e:
224
+ return web.json_response({"error": "Failed to create directory", "details": str(e)}, status=500)
225
+
226
+ @prompt_server.routes.post("/api/huggingface/create_model_type")
227
+ async def route_create_model_type(request):
228
+ """Create a new first-level folder under the main models directory."""
229
+ try:
230
+ data = await request.json()
231
+ name = (data.get("name") or "").strip()
232
+ if not name:
233
+ return web.json_response({"error": "Missing 'name'"}, status=400)
234
+
235
+ # Sanitize folder name to a single path component
236
+ from ...utils.helpers import sanitize_filename
237
+ safe = sanitize_filename(name)
238
+ if not safe:
239
+ return web.json_response({"error": "Invalid folder name"}, status=400)
240
+
241
+ # Resolve models directory
242
+ models_dir = getattr(folder_paths, 'models_dir', None)
243
+ if not models_dir:
244
+ base = getattr(folder_paths, 'base_path', os.getcwd())
245
+ models_dir = os.path.join(base, 'models')
246
+
247
+ abs_path = os.path.abspath(os.path.join(models_dir, safe))
248
+ # Ensure it remains inside models_dir
249
+ if os.path.commonpath([abs_path, os.path.abspath(models_dir)]) != os.path.abspath(models_dir):
250
+ return web.json_response({"error": "Invalid path"}, status=400)
251
+
252
+ os.makedirs(abs_path, exist_ok=True)
253
+ return web.json_response({"success": True, "name": safe, "path": abs_path})
254
+ except Exception as e:
255
+ return web.json_response({"error": "Failed to create model type folder", "details": str(e)}, status=500)
256
+
257
+ @prompt_server.routes.get("/api/huggingface/model_roots")
258
+ async def route_get_model_roots(request):
259
+ """Return all known root directories for a model type (ComfyUI + plugin custom roots)."""
260
+ model_type = request.query.get("type", "checkpoints").lower().strip()
261
+ roots = _get_all_roots_for_type(model_type)
262
+ global_root = get_global_default_root()
263
+ return web.json_response({
264
+ "model_type": model_type,
265
+ "roots": roots,
266
+ "global_root": global_root or "",
267
+ "effective_root": _get_effective_base_dir(model_type),
268
+ })
269
+
270
+ @prompt_server.routes.post("/api/huggingface/create_root")
271
+ async def route_create_model_root(request):
272
+ """Create a new root directory for a model type and register it in plugin config.
273
+ Note: ComfyUI may require restart to recognize this root globally; the plugin uses it immediately.
274
+ """
275
+ try:
276
+ data = await request.json()
277
+ model_type = (data.get("model_type") or "checkpoints").lower().strip()
278
+ abs_path = os.path.expanduser((data.get("path") or "").strip())
279
+ if not abs_path:
280
+ return web.json_response({"error": "Missing 'path'"}, status=400)
281
+ if not os.path.isabs(abs_path):
282
+ return web.json_response({"error": "Path must be absolute"}, status=400)
283
+ # Normalize to absolute path
284
+ abs_path = os.path.abspath(abs_path)
285
+ # Create directory if missing
286
+ os.makedirs(abs_path, exist_ok=True)
287
+
288
+ type_key = get_model_type_folder_name(model_type)
289
+ roots = _load_custom_roots()
290
+ current = roots.get(type_key, [])
291
+ if abs_path not in current:
292
+ current.append(abs_path)
293
+ roots[type_key] = current
294
+ if not _save_custom_roots(roots):
295
+ return web.json_response({"error": "Failed to persist custom root"}, status=500)
296
+ return web.json_response({"success": True, "path": abs_path})
297
+ except Exception as e:
298
+ return web.json_response({"error": "Failed to create root", "details": str(e)}, status=500)
299
+
300
+ @prompt_server.routes.get("/api/huggingface/global_root")
301
+ async def route_get_global_root(request):
302
+ """Return the persisted global download root (if configured)."""
303
+ global_root = get_global_default_root()
304
+ return web.json_response({
305
+ "global_root": global_root or "",
306
+ "enabled": bool(global_root),
307
+ })
308
+
309
+ @prompt_server.routes.post("/api/huggingface/global_root")
310
+ async def route_set_global_root(request):
311
+ """Persist a global download root used as: <global_root>/<model_type_folder>."""
312
+ try:
313
+ data = await request.json()
314
+ path = os.path.expanduser((data.get("path") or "").strip())
315
+ if not path:
316
+ return web.json_response({"error": "Missing 'path'"}, status=400)
317
+ if not os.path.isabs(path):
318
+ return web.json_response({"error": "Path must be absolute"}, status=400)
319
+
320
+ abs_path = os.path.abspath(path)
321
+ os.makedirs(abs_path, exist_ok=True)
322
+ if not _set_global_default_root(abs_path):
323
+ return web.json_response({"error": "Failed to persist global root"}, status=500)
324
+
325
+ return web.json_response({"success": True, "global_root": abs_path})
326
+ except Exception as e:
327
+ return web.json_response({"error": "Failed to set global root", "details": str(e)}, status=500)
328
+
329
+ @prompt_server.routes.post("/api/huggingface/global_root/clear")
330
+ async def route_clear_global_root(request):
331
+ """Clear the persisted global download root."""
332
+ try:
333
+ if not _set_global_default_root(None):
334
+ return web.json_response({"error": "Failed to clear global root"}, status=500)
335
+ return web.json_response({"success": True})
336
+ except Exception as e:
337
+ return web.json_response({"error": "Failed to clear global root", "details": str(e)}, status=500)
custom_nodes/ComfyUI-HuggingFace/server/routes/GetModelTypes.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ================================================
2
+ # File: server/routes/GetModelTypes.py
3
+ # ================================================
4
+ import os
5
+ from aiohttp import web
6
+ import server # ComfyUI server instance
7
+ import folder_paths
8
+
9
+ prompt_server = server.PromptServer.instance
10
+
11
+ @prompt_server.routes.get("/api/huggingface/model_types")
12
+ async def route_get_model_types(request):
13
+ """API Endpoint to get the known model types and their mapping."""
14
+ try:
15
+ # Dynamically list all first-level folders under the main models directory
16
+ models_dir = getattr(folder_paths, 'models_dir', None)
17
+ if not models_dir:
18
+ base = getattr(folder_paths, 'base_path', os.getcwd())
19
+ models_dir = os.path.join(base, 'models')
20
+ if not os.path.isdir(models_dir):
21
+ return web.json_response({})
22
+
23
+ entries = {}
24
+ for name in sorted(os.listdir(models_dir)):
25
+ p = os.path.join(models_dir, name)
26
+ if os.path.isdir(p):
27
+ entries[name] = name
28
+ return web.json_response(entries)
29
+ except Exception as e:
30
+ print(f"Error getting model types: {e}")
31
+ return web.json_response({"error": "Internal Server Error", "details": str(e), "status_code": 500}, status=500)
custom_nodes/ComfyUI-HuggingFace/server/routes/GetStatus.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ================================================
2
+ # File: server/routes/GetStatus.py
3
+ # ================================================
4
+ from aiohttp import web
5
+ import server # ComfyUI server instance
6
+ from ...downloader.manager import manager as download_manager
7
+
8
+ prompt_server = server.PromptServer.instance
9
+
10
+ @prompt_server.routes.get("/api/huggingface/status")
11
+ async def route_get_status(request):
12
+ """API Endpoint to get the status of downloads."""
13
+ try:
14
+ status = download_manager.get_status()
15
+ return web.json_response(status)
16
+ except Exception as e:
17
+ print(f"Error getting download status: {e}")
18
+ # Format error response consistently
19
+ return web.json_response({"error": "Internal Server Error", "details": f"Failed to get status: {str(e)}", "status_code": 500}, status=500)
custom_nodes/ComfyUI-HuggingFace/server/routes/OpenPath.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ================================================
2
+ # File: server/routes/OpenPath.py
3
+ # ================================================
4
+ import asyncio
5
+ import json
6
+ from aiohttp import web
7
+
8
+ import server # ComfyUI server instance
9
+ from ...downloader.manager import manager as download_manager
10
+
11
+ prompt_server = server.PromptServer.instance
12
+
13
+ @prompt_server.routes.post("/api/huggingface/open_path")
14
+ async def route_open_path(request):
15
+ """API Endpoint to open the containing folder of a completed download."""
16
+ if not download_manager:
17
+ return web.json_response({"error": "Download Manager not initialized"}, status=500)
18
+
19
+ try:
20
+ data = await request.json()
21
+ download_id = data.get("download_id")
22
+
23
+ if not download_id:
24
+ return web.json_response({"error": "Missing 'download_id'", "details": "The request body must contain the 'download_id' of the completed item."}, status=400)
25
+
26
+ print(f"[API Route /huggingface/open_path] Received open path request for ID: {download_id}")
27
+ # Call manager method in thread
28
+ result = await asyncio.to_thread(download_manager.open_containing_folder, download_id)
29
+
30
+ status_code = 200 if result.get("success") else 404 if "not found" in result.get("error", "").lower() else 400 # Use 400 for OS error, security etc
31
+ # Check for specific errors to return better codes
32
+ if not result.get("success"):
33
+ error_lower = result.get("error", "").lower()
34
+ if "directory does not exist" in error_lower or "id not found" in error_lower:
35
+ status_code = 404
36
+ elif "cannot open path" in error_lower or "unsupported os" in error_lower or "failed to open" in error_lower or "xdg-open" in error_lower:
37
+ status_code = 501 # Not Implemented / Failed on server side
38
+ elif "must be 'completed'" in error_lower:
39
+ status_code = 409 # Conflict - wrong state
40
+ else:
41
+ status_code = 400 # Bad Request / general failure
42
+
43
+ # Prevent sensitive path info leakage in error messages by default
44
+ if not result.get("success") and "error" in result and status_code != 200:
45
+ print(f"[API Route /huggingface/open_path] Error for ID {download_id}: {result['error']}") # Log full error on server
46
+ # Optionally sanitize error sent to client
47
+ # if "Directory:" in result["error"] or "Path:" in result["error"]:
48
+ # result["error"] = "Server failed to open the specified directory."
49
+
50
+ return web.json_response(result, status=status_code)
51
+
52
+ except json.JSONDecodeError:
53
+ return web.json_response({"error": "Invalid JSON body"}, status=400)
54
+ except Exception as e:
55
+ import traceback
56
+ print(f"Error handling /huggingface/open_path request for ID '{data.get('download_id', 'N/A')}': {e}")
57
+ # traceback.print_exc()
58
+ return web.json_response({"error": "Internal Server Error", "details": f"An unexpected error occurred: {str(e)}"}, status=500)
custom_nodes/ComfyUI-HuggingFace/server/routes/RetryDownload.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ================================================
2
+ # File: server/routes/RetryDownload.py
3
+ # ================================================
4
+ import asyncio
5
+ import json
6
+ from aiohttp import web
7
+
8
+ import server # ComfyUI server instance
9
+ from ...downloader.manager import manager as download_manager
10
+
11
+ prompt_server = server.PromptServer.instance
12
+
13
+ @prompt_server.routes.post("/api/huggingface/retry")
14
+ async def route_retry_download(request):
15
+ """API Endpoint to retry a failed/cancelled download."""
16
+ if not download_manager:
17
+ return web.json_response({"error": "Download Manager not initialized"}, status=500)
18
+
19
+ try:
20
+ data = await request.json()
21
+ download_id = data.get("download_id")
22
+
23
+ if not download_id:
24
+ return web.json_response({"error": "Missing 'download_id'", "details": "The request body must contain the 'download_id' of the item to retry."}, status=400)
25
+
26
+ print(f"[API Route /huggingface/retry] Received retry request for ID: {download_id}")
27
+ # Call manager method (which handles locking)
28
+ result = await asyncio.to_thread(download_manager.retry_download, download_id) # Run sync manager method in thread
29
+
30
+ status_code = 200 if result.get("success") else 404 if "not found" in result.get("error", "").lower() else 400
31
+ return web.json_response(result, status=status_code)
32
+
33
+ except json.JSONDecodeError:
34
+ return web.json_response({"error": "Invalid JSON body"}, status=400)
35
+ except Exception as e:
36
+ import traceback
37
+ print(f"Error handling /huggingface/retry request for ID '{data.get('download_id', 'N/A')}': {e}")
38
+ # traceback.print_exc() # Uncomment for detailed logs
39
+ return web.json_response({"error": "Internal Server Error", "details": f"An unexpected error occurred: {str(e)}"}, status=500)
custom_nodes/ComfyUI-HuggingFace/server/routes/SearchModels.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ================================================
2
+ # File: server/routes/SearchModels.py
3
+ # ================================================
4
+ import json
5
+ import traceback
6
+ from aiohttp import web
7
+
8
+ import server # ComfyUI server instance
9
+ from ..utils import get_request_json, resolve_huggingface_api_key
10
+ from ...config import HUGGINGFACE_API_TYPE_MAP
11
+
12
+ prompt_server = server.PromptServer.instance
13
+
14
+ @prompt_server.routes.post("/api/huggingface/search")
15
+ async def route_search_models(request):
16
+ """API Endpoint for searching models using huggingface_hub."""
17
+ try:
18
+ data = await get_request_json(request)
19
+
20
+ query = data.get("query", "").strip()
21
+ model_type_keys = data.get("model_types", []) # e.g., ["lora", "checkpoint"]
22
+ base_model_filters = data.get("base_models", []) # e.g., ["SD 1.5", "Pony"]
23
+ sort = data.get("sort", "Most Downloaded")
24
+ limit = int(data.get("limit", 20))
25
+ page = int(data.get("page", 1))
26
+ resolved_api_key = resolve_huggingface_api_key(data)
27
+ nsfw = data.get("nsfw", True) # Default to True (enabled)
28
+
29
+ if not query and not model_type_keys and not base_model_filters:
30
+ raise web.HTTPBadRequest(reason="Search requires a query or at least one filter (type or base model).")
31
+
32
+ # Use huggingface_hub for search
33
+ try:
34
+ from huggingface_hub import HfApi
35
+ hf_api = HfApi(token=resolved_api_key)
36
+
37
+ # Prepare search parameters
38
+ search_params = {
39
+ "limit": limit
40
+ }
41
+
42
+ # Map sort values to valid huggingface_hub values
43
+ sort_mapping = {
44
+ "Most Downloaded": "downloads",
45
+ "Most Liked": "likes",
46
+ "Newest": "created_at",
47
+ "Relevancy": None, # Remove sort for relevancy (default)
48
+ "Alphabetical": "modelId"
49
+ }
50
+
51
+ sort_value = sort_mapping.get(sort, None)
52
+ if sort_value:
53
+ search_params["sort"] = sort_value
54
+
55
+ if query:
56
+ search_params["search"] = query
57
+
58
+ # Map model types to tags
59
+ if model_type_keys:
60
+ tags = []
61
+ for key in model_type_keys:
62
+ api_type = HUGGINGFACE_API_TYPE_MAP.get(key.lower())
63
+ if api_type:
64
+ tags.append(api_type.lower())
65
+ if tags:
66
+ search_params["tags"] = tags
67
+
68
+ # Add base model filters as part of query if specified
69
+ if base_model_filters:
70
+ base_model_query = " ".join([f'base_model:{bm}' for bm in base_model_filters])
71
+ if query:
72
+ search_params["search"] = f"{query} {base_model_query}"
73
+ else:
74
+ search_params["search"] = base_model_query
75
+
76
+ print(f"[Server Search] huggingface_hub: search='{search_params.get('search', '<none>')}', tags={search_params.get('tags', 'Any')}, sort={sort}, limit={limit}")
77
+
78
+ # Perform search
79
+ models = hf_api.list_models(**search_params)
80
+
81
+ # Format results for frontend
82
+ formatted_models = []
83
+ for model in models:
84
+ formatted_model = {
85
+ "id": model.id,
86
+ "name": model.id.split('/')[-1],
87
+ "description": model.tags or "",
88
+ "creator": {"username": model.author or ""},
89
+ "modelId": model.id,
90
+ "downloads": model.downloads or 0,
91
+ "likes": model.likes or 0,
92
+ "tags": model.tags or [],
93
+ "modelType": "Unknown", # Will be determined by frontend
94
+ "baseModel": [], # Will be determined by frontend
95
+ "stats": {
96
+ "downloadCount": model.downloads or 0,
97
+ "thumbsUpCount": model.likes or 0
98
+ }
99
+ }
100
+ formatted_models.append(formatted_model)
101
+
102
+ response_data = {
103
+ "items": formatted_models,
104
+ "metadata": {
105
+ "currentPage": page,
106
+ "pageSize": limit,
107
+ "totalItems": len(formatted_models),
108
+ "totalPages": 1 # huggingface_hub doesn't provide pagination info
109
+ }
110
+ }
111
+
112
+ return web.json_response(response_data)
113
+
114
+ except Exception as e:
115
+ print(f"[Server Search] huggingface_hub search failed: {e}")
116
+ return web.json_response({"error": "Search failed", "details": str(e)}, status=500)
117
+
118
+ except Exception as e:
119
+ print(f"Error in search_models: {e}")
120
+ traceback.print_exc()
121
+ return web.json_response({"error": "Internal Server Error", "details": str(e)}, status=500)
custom_nodes/ComfyUI-HuggingFace/server/routes/__init__.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ================================================
2
+ # File: server/routes/__init__.py
3
+ # ================================================
4
+ # This file imports all the individual route modules.
5
+ # When the `routes` package is imported by `server/__init__.py`,
6
+ # these imports will be executed, registering the routes with the
7
+ # ComfyUI server instance.
8
+
9
+ from . import CancelDownload
10
+ from . import ClearHistory
11
+ from . import DownloadModel
12
+ from . import GetBaseModels
13
+ from . import GetModelDetails
14
+ from . import GetModelTypes
15
+ from . import GetModelDirs
16
+ from . import GetStatus
17
+ from . import OpenPath
18
+ from . import RetryDownload
19
+ from . import SearchModels
20
+
21
+ print("[HuggingFace] All server route modules loaded.")
custom_nodes/ComfyUI-HuggingFace/server/utils.py ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ================================================
2
+ # File: server/utils.py
3
+ # ================================================
4
+ import json
5
+ import os
6
+ from typing import Any, Dict, Optional
7
+ from aiohttp import web
8
+
9
+ # Import necessary components from our modules
10
+ from ..api.huggingface import HuggingFaceAPI
11
+ from ..utils.helpers import parse_huggingface_input
12
+
13
+ async def get_request_json(request):
14
+ """Safely get JSON data from request."""
15
+ try:
16
+ return await request.json()
17
+ except Exception as e:
18
+ print(f"Error parsing request JSON: {e}")
19
+ raise web.HTTPBadRequest(reason=f"Invalid JSON format: {e}")
20
+
21
+ def resolve_huggingface_api_key(payload: Optional[Dict[str, Any]] = None) -> Optional[str]:
22
+ """
23
+ Resolve API key priority:
24
+ 1) Explicit key from request payload (`api_key`)
25
+ 2) `HUGGINGFACE_TOKEN` environment variable
26
+ """
27
+ request_key = ""
28
+ if isinstance(payload, dict):
29
+ raw_key = payload.get("api_key", "")
30
+ if isinstance(raw_key, str):
31
+ request_key = raw_key.strip()
32
+
33
+ if request_key:
34
+ return request_key
35
+
36
+ env_key = os.getenv("HUGGINGFACE_TOKEN", "").strip()
37
+ if env_key:
38
+ return env_key
39
+
40
+ return None
41
+
42
+ async def get_huggingface_model_and_version_details(api: HuggingFaceAPI, model_url_or_id: str, req_version_id: Optional[int]) -> Dict[str, Any]:
43
+ """
44
+ Helper to fetch HuggingFace details.
45
+ Prioritizes fetching model info based on resolved Model ID.
46
+ Fetches specific version info if version ID is provided/resolved, otherwise latest.
47
+ Returns a dict with 'model_info', 'version_info', 'primary_file', and resolved IDs.
48
+ Raises HTTP exceptions on critical failures.
49
+ """
50
+ target_model_id = None
51
+ target_version_id = None
52
+ potential_version_id_from_input = None
53
+ model_info = {}
54
+ version_info_to_use = {} # The version (specific or latest) whose file we'll use
55
+ primary_file = None
56
+
57
+ # --- 1. Parse Input to get potential IDs ---
58
+ parsed_model_id, parsed_version_id = parse_huggingface_input(model_url_or_id)
59
+
60
+ # Determine the initial target model ID (input URL/ID takes precedence)
61
+ target_model_id = parsed_model_id
62
+
63
+ # Determine the specific version requested (explicit param > URL param)
64
+ if req_version_id and str(req_version_id).isdigit():
65
+ try:
66
+ potential_version_id_from_input = int(req_version_id)
67
+ except (ValueError, TypeError):
68
+ print(f"[API Helper] Warning: Invalid req_version_id: {req_version_id}. Ignoring.")
69
+ elif parsed_version_id:
70
+ potential_version_id_from_input = parsed_version_id
71
+
72
+ # --- 2. Ensure we have a Model ID ---
73
+ # If we only got a version ID from the input (e.g., huggingface.com/model-versions/456),
74
+ # we need to fetch that version *first* just to find the model ID.
75
+ if not target_model_id and potential_version_id_from_input:
76
+ print(f"[API Helper] Input requires fetching version {potential_version_id_from_input} first to find model ID.")
77
+ temp_version_info = api.get_model_version_info(potential_version_id_from_input)
78
+ if temp_version_info and "error" not in temp_version_info and temp_version_info.get('modelId'):
79
+ target_model_id = temp_version_info['modelId']
80
+ print(f"[API Helper] Found Model ID {target_model_id} from Version ID {potential_version_id_from_input}.")
81
+ # We might reuse temp_version_info later if this was the specifically requested version
82
+ else:
83
+ err = temp_version_info.get('details', 'Could not find model ID from version') if isinstance(temp_version_info, dict) else 'API error'
84
+ raise web.HTTPNotFound(reason=f"Could not determine Model ID from Version ID {potential_version_id_from_input}", body=json.dumps({"error": f"Version {potential_version_id_from_input} not found or missing modelId", "details": err}))
85
+
86
+ # If still no model ID after potential lookup, fail
87
+ if not target_model_id:
88
+ raise web.HTTPBadRequest(reason="Could not determine a valid Model ID from the input.")
89
+
90
+ # --- 3. Fetch Core Model Information (Always based on target_model_id) ---
91
+ print(f"[API Helper] Fetching core model info for Model ID: {target_model_id}")
92
+ model_info_result = api.get_model_info(target_model_id)
93
+ if not model_info_result or "error" in model_info_result:
94
+ err_details = model_info_result.get('details', 'Unknown API error') if isinstance(model_info_result, dict) else 'Unknown API error'
95
+ raise web.HTTPNotFound(reason=f"Model {target_model_id} not found or API error", body=json.dumps({"error": f"Model {target_model_id} not found or API error", "details": err_details}))
96
+ model_info = model_info_result # Store the successfully fetched model info
97
+
98
+ # --- 4. Determine and Fetch Version Info for File Details ---
99
+ if potential_version_id_from_input:
100
+ # User specified a version explicitly, fetch its details
101
+ print(f"[API Helper] Fetching specific version info for Version ID: {potential_version_id_from_input}")
102
+ target_version_id = potential_version_id_from_input # This is the version we need info for
103
+ # Check if we already fetched this during Model ID lookup
104
+ if 'temp_version_info' in locals() and temp_version_info.get('id') == target_version_id:
105
+ print("[API Helper] Reusing version info fetched earlier.")
106
+ version_info_to_use = temp_version_info
107
+ else:
108
+ version_info_result = api.get_model_version_info(target_version_id)
109
+ if not version_info_result or "error" in version_info_result:
110
+ err_details = version_info_result.get('details', 'Unknown API error') if isinstance(version_info_result, dict) else 'Unknown API error'
111
+ raise web.HTTPNotFound(reason=f"Specified Version {target_version_id} not found or API error", body=json.dumps({"error": f"Version {target_version_id} not found or API error", "details": err_details}))
112
+ version_info_to_use = version_info_result
113
+ else:
114
+ # No specific version requested, find latest/default from model_info
115
+ print(f"[API Helper] Finding latest/default version for Model ID: {target_model_id}")
116
+ versions = model_info.get("modelVersions")
117
+ if not versions or not isinstance(versions, list) or len(versions) == 0:
118
+ raise web.HTTPNotFound(reason=f"Model {target_model_id} has no listed model versions.")
119
+
120
+ # Find the 'best' default version (usually first published)
121
+ default_version_summary = next((v for v in versions if v.get('status') == 'Published'), versions[0])
122
+ target_version_id = default_version_summary.get('id')
123
+ if not target_version_id:
124
+ raise web.HTTPNotFound(reason=f"Model {target_model_id}'s latest version has no ID.")
125
+
126
+ print(f"[API Helper] Using latest/default Version ID: {target_version_id}. Fetching its full details.")
127
+ # Fetch full details for this latest version
128
+ version_info_result = api.get_model_version_info(target_version_id)
129
+ if not version_info_result or "error" in version_info_result:
130
+ # Log error, but maybe try to proceed with summary data if desperate? Risky.
131
+ err_details = version_info_result.get('details', 'Unknown error getting full version') if isinstance(version_info_result, dict) else 'Error'
132
+ print(f"[API Helper] Warning: Could not fetch full details for latest version {target_version_id}. Details: {err_details}. Falling back to summary.")
133
+ # Use summary data from model_info as fallback - file info might be missing!
134
+ version_info_to_use = default_version_summary
135
+ # Ensure minimal structure for file finding later
136
+ version_info_to_use['files'] = version_info_to_use.get('files', [])
137
+ version_info_to_use['images'] = version_info_to_use.get('images', [])
138
+ version_info_to_use['modelId'] = version_info_to_use.get('modelId', target_model_id) # Ensure modelId is present
139
+ version_info_to_use['model'] = version_info_to_use.get('model', {'name': model_info.get('name', 'Unknown')}) # Add fallback model name
140
+
141
+ else:
142
+ version_info_to_use = version_info_result
143
+
144
+ # --- 5. Find Primary File from the Determined Version (version_info_to_use) ---
145
+ print(f"[API Helper] Finding primary file for Version ID: {target_version_id}")
146
+ files = version_info_to_use.get("files", [])
147
+ if not isinstance(files, list): files = []
148
+
149
+ # Handle fallback downloadUrl at version level if 'files' is empty/missing
150
+ if not files and 'downloadUrl' in version_info_to_use and version_info_to_use['downloadUrl']:
151
+ print("[API Helper] Warning: No 'files' array found, using version-level 'downloadUrl'.")
152
+ files = [{
153
+ "id": None, "name": version_info_to_use.get('name', f"version_{target_version_id}_file"),
154
+ "primary": True, "type": "Model", "sizeKB": version_info_to_use.get('fileSizeKB'),
155
+ "downloadUrl": version_info_to_use['downloadUrl'], "hashes": {}, "metadata": {}
156
+ }]
157
+
158
+ if not files:
159
+ raise web.HTTPNotFound(reason=f"Version {target_version_id} (Name: {version_info_to_use.get('name', 'N/A')}) has no files listed.")
160
+
161
+ # For HuggingFace, just pick the first file or largest .safetensors
162
+ primary_file = None
163
+ if files and isinstance(files, list):
164
+ # Try to find .safetensors first
165
+ for file_info in files:
166
+ if (file_info.get("type") == "file" and
167
+ file_info.get("path", "").endswith(".safetensors")):
168
+ primary_file = file_info
169
+ break
170
+
171
+ # If no .safetensors, pick the largest file
172
+ if not primary_file:
173
+ primary_file = max(files, key=lambda x: x.get("size", 0), default=None)
174
+
175
+ if not primary_file:
176
+ raise web.HTTPNotFound(reason=f"Could not find any usable file with a download URL for version {target_version_id}.")
177
+
178
+ print(f"[API Helper] Selected file: Name='{primary_file.get('name', 'N/A')}', SizeKB={primary_file.get('sizeKB')}")
179
+
180
+ # --- 6. Return Results ---
181
+ return {
182
+ "model_info": model_info, # Always the full model info
183
+ "version_info": version_info_to_use, # Info for the specific/latest version
184
+ "primary_file": primary_file, # The file from that version
185
+ "target_model_id": target_model_id, # Resolved model ID
186
+ "target_version_id": target_version_id, # Resolved version ID (specific or latest)
187
+ }
custom_nodes/ComfyUI-HuggingFace/utils/__init__.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ================================================
2
+ # File: utils/__init__.py
3
+ # ================================================
4
+
5
+ # Import utility functions for easy access
6
+ from .helpers import parse_huggingface_input, get_model_dir, sanitize_filename
7
+ from . import helpers
8
+
9
+ # Make functions available at package level
10
+ __all__ = [
11
+ 'parse_huggingface_input',
12
+ 'get_model_dir',
13
+ 'sanitize_filename',
14
+ 'helpers'
15
+ ]
custom_nodes/ComfyUI-HuggingFace/utils/helpers.py ADDED
@@ -0,0 +1,224 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ================================================
2
+ # File: utils/helpers.py
3
+ # ================================================
4
+ import os
5
+ import urllib.parse
6
+ import re
7
+ from pathlib import Path
8
+ from typing import Optional, List, Dict, Any
9
+
10
+ import folder_paths
11
+
12
+ # Import config values needed here
13
+ from ..config import PLUGIN_ROOT, MODEL_TYPE_DIRS
14
+
15
+ # Canonical aliases for model type/folder names. Values are preferred folder names.
16
+ MODEL_TYPE_ALIASES = {
17
+ "checkpoint": "checkpoints",
18
+ "checkpoints": "checkpoints",
19
+ "diffusionmodel": "diffusion_models",
20
+ "diffusionmodels": "diffusion_models",
21
+ "diffusion_model": "diffusion_models",
22
+ "diffusion_models": "diffusion_models", # Wan 2.2 and similar go here
23
+ "diffusers": "diffusers",
24
+ "unet": "unet", # GGUF models go here
25
+ "lora": "loras",
26
+ "loras": "loras",
27
+ "locon": "loras",
28
+ "lycoris": "loras",
29
+ "vae": "vae",
30
+ "embedding": "embeddings",
31
+ "embeddings": "embeddings",
32
+ "textualinversion": "embeddings",
33
+ "hypernetwork": "hypernetworks",
34
+ "hypernetworks": "hypernetworks",
35
+ "controlnet": "controlnet",
36
+ "upscaler": "upscale_models",
37
+ "upscalers": "upscale_models",
38
+ "upscale_model": "upscale_models",
39
+ "upscale_models": "upscale_models",
40
+ "motionmodule": "motion_models",
41
+ "motionmodules": "motion_models",
42
+ "motion_model": "motion_models",
43
+ "motion_models": "motion_models",
44
+ }
45
+
46
+ MODEL_TYPE_ALIASES_COMPACT = {
47
+ re.sub(r'[^a-z0-9]', '', k): v for k, v in MODEL_TYPE_ALIASES.items()
48
+ }
49
+
50
+ def _normalize_model_type(model_type: str) -> str:
51
+ """Normalize model type string to canonical form."""
52
+ if not model_type:
53
+ return "other"
54
+
55
+ normalized = model_type.lower().strip()
56
+
57
+ # Try exact match first
58
+ if normalized in MODEL_TYPE_ALIASES:
59
+ return MODEL_TYPE_ALIASES[normalized]
60
+
61
+ # Try compact match (remove non-alphanumeric chars)
62
+ compact = re.sub(r'[^a-z0-9]', '', normalized)
63
+ if compact in MODEL_TYPE_ALIASES_COMPACT:
64
+ return MODEL_TYPE_ALIASES_COMPACT[compact]
65
+
66
+ return "other"
67
+
68
+ def get_model_dir(model_type: str, explicit_save_root: str = "", selected_subdir: str = "") -> Optional[str]:
69
+ """Get the directory path for a given model type."""
70
+ try:
71
+ # Normalize the model type
72
+ normalized_type = _normalize_model_type(model_type)
73
+
74
+ # Use explicit root if provided
75
+ if explicit_save_root:
76
+ base_path = explicit_save_root
77
+ # If selected_subdir is provided, append it
78
+ if selected_subdir:
79
+ base_path = os.path.join(base_path, selected_subdir)
80
+ return base_path
81
+
82
+ # Try ComfyUI's folder_paths first
83
+ try:
84
+ if normalized_type in ["checkpoints", "loras", "vae", "embeddings", "hypernetworks", "controlnet", "upscale_models"]:
85
+ return folder_paths.get_folder_paths(normalized_type)[0]
86
+ elif normalized_type in ["diffusion_models", "motion_models", "unet", "diffusers"]:
87
+ # These might not be standard ComfyUI types, try to get them
88
+ try:
89
+ return folder_paths.get_folder_paths(normalized_type)[0]
90
+ except:
91
+ # Fallback: try unet for diffusion_models
92
+ if normalized_type == "diffusion_models":
93
+ try:
94
+ return folder_paths.get_folder_paths("diffusion_models")[0]
95
+ except:
96
+ # If diffusion_models doesn't exist, try unet
97
+ try:
98
+ return folder_paths.get_folder_paths("unet")[0]
99
+ except:
100
+ # Fallback to checkpoints
101
+ return folder_paths.get_folder_paths("checkpoints")[0]
102
+ elif normalized_type == "unet":
103
+ try:
104
+ return folder_paths.get_folder_paths("unet")[0]
105
+ except:
106
+ # Fallback to checkpoints
107
+ return folder_paths.get_folder_paths("checkpoints")[0]
108
+ else:
109
+ # For other types, try diffusion_models first
110
+ try:
111
+ return folder_paths.get_folder_paths("diffusion_models")[0]
112
+ except:
113
+ # Fallback to checkpoints
114
+ return folder_paths.get_folder_paths("checkpoints")[0]
115
+ else:
116
+ # For other types, use our extension directory instead of custom_nodes
117
+ from ..config import PLUGIN_ROOT
118
+ return os.path.join(PLUGIN_ROOT, "other_models")
119
+
120
+ except:
121
+ # Fallback to base_path + type
122
+ return os.path.join(folder_paths.base_path, normalized_type)
123
+
124
+ except Exception as e:
125
+ print(f"Error getting model directory for {model_type}: {e}")
126
+ return None
127
+
128
+ def sanitize_filename(filename: str) -> str:
129
+ """Sanitize filename for safe file system usage."""
130
+ if not filename:
131
+ return "unnamed_file"
132
+
133
+ # Remove invalid characters
134
+ sanitized = re.sub(r'[<>:"/\\|?*]', '_', filename)
135
+
136
+ # Remove control characters
137
+ sanitized = re.sub(r'[\x00-\x1f\x7f]', '', sanitized)
138
+
139
+ # Limit length
140
+ if len(sanitized) > 255:
141
+ name, ext = os.path.splitext(sanitized)
142
+ sanitized = name[:255-len(ext)] + ext
143
+
144
+ return sanitized.strip()
145
+
146
+ def parse_huggingface_input(url_or_id: str) -> tuple[str | None, str | None]:
147
+ """
148
+ Parses HuggingFace URL or ID string.
149
+ Returns: (model_id, filename) tuple. Both can be None.
150
+ Handles URLs like:
151
+ - https://huggingface.co/FX-FeiHou/wan2.2-Remix/resolve/main/NSFW/Wan2.2_Remix_NSFW_i2v_14b_high_lighting_fp8_e4m3fn_v2.1.safetensors
152
+ - FX-FeiHou/wan2.2-Remix
153
+ """
154
+ if not url_or_id:
155
+ return None, None
156
+
157
+ url_or_id = str(url_or_id).strip()
158
+ model_id: str | None = None
159
+ filename: str | None = None
160
+
161
+ # Check if it's a direct download URL
162
+ if "/resolve/main/" in url_or_id:
163
+ try:
164
+ parsed_url = urllib.parse.urlparse(url_or_id)
165
+ print(f"[DEBUG] Parsed URL: {parsed_url}")
166
+ print(f"[DEBUG] Path parts: {parsed_url.path.split('/')}")
167
+
168
+ # Extract model ID from path
169
+ path_parts = parsed_url.path.split('/')
170
+ print(f"[DEBUG] Path parts length: {len(path_parts)}")
171
+ print(f"[DEBUG] Path parts[2]: {path_parts[2] if len(path_parts) > 2 else 'None'}")
172
+
173
+ if len(path_parts) >= 4 and path_parts[3] == "resolve":
174
+ # URL format: /FX-FeiHou/wan2.2-Remix/resolve/main/NSFW/file.safetensors
175
+ model_id = "/".join(path_parts[1:3]) # FX-FeiHou/wan2.2-Remix
176
+ filename = "/".join(path_parts[4:]) # main/NSFW/file.safetensors
177
+ # Remove "main/" from filename if it's duplicated
178
+ if filename.startswith("main/"):
179
+ filename = filename[5:] # Remove "main/" prefix
180
+ print(f"[DEBUG] Extracted model_id: {model_id}")
181
+ print(f"[DEBUG] Extracted filename: {filename}")
182
+ print(f"Parsed HF download URL - Model: {model_id}, File: {filename}")
183
+ return model_id, filename
184
+ else:
185
+ print(f"[DEBUG] Path does not match expected format")
186
+ except Exception as e:
187
+ print(f"Warning: Could not parse HF download URL '{url_or_id}': {e}")
188
+ return None, None
189
+
190
+ # Check if it's a simple model ID (username/repo format)
191
+ if "/" in url_or_id and not url_or_id.startswith("http"):
192
+ parts = url_or_id.split("/")
193
+ if len(parts) >= 2:
194
+ model_id = "/".join(parts[0:2])
195
+ print(f"Parsed HF model ID: {model_id}")
196
+ return model_id, None
197
+
198
+ # Try parsing as full URL
199
+ try:
200
+ parsed_url = urllib.parse.urlparse(url_or_id)
201
+
202
+ if "huggingface.co" in parsed_url.netloc.lower():
203
+ path_parts = parsed_url.path.split('/')
204
+ if len(path_parts) >= 3:
205
+ model_id = "/".join(path_parts[1:3]) # Extract username/repo
206
+ print(f"Parsed HF URL - Model: {model_id}")
207
+ return model_id, None
208
+ except Exception as e:
209
+ print(f"Warning: Could not parse HF URL '{url_or_id}': {e}")
210
+
211
+ print(f"Input '{url_or_id}' is not a recognizable HF model ID or URL.")
212
+ return None, None
213
+
214
+ def get_model_folder_paths(model_type: str) -> List[str]:
215
+ """Get all folder paths for a given model type."""
216
+ try:
217
+ normalized_type = _normalize_model_type(model_type)
218
+ return folder_paths.get_folder_paths(normalized_type)
219
+ except:
220
+ return []
221
+
222
+ def get_model_type_folder_name(model_type: str) -> str:
223
+ """Get the standard folder name for a model type."""
224
+ return _normalize_model_type(model_type)
custom_nodes/ComfyUI-HuggingFace/web/js/api/huggingface.js ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // API client for HuggingFace UI
2
+ // Wraps ComfyUI's fetchApi with consistent error handling
3
+
4
+ import { api } from "../../../../scripts/api.js";
5
+
6
+ export class HuggingFaceDownloaderAPI {
7
+ static async _request(endpoint, options = {}) {
8
+ try {
9
+ const url = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
10
+ const response = await api.fetchApi(url, options);
11
+
12
+ if (!response.ok) {
13
+ let errorData;
14
+ const status = response.status;
15
+ const statusText = response.statusText;
16
+ try {
17
+ errorData = await response.json();
18
+ if (typeof errorData !== "object" || errorData === null) {
19
+ errorData = { detail: String(errorData) };
20
+ }
21
+ } catch (_) {
22
+ const detailText = await response.text().catch(() => `Status ${status} - Could not read error text`);
23
+ errorData = {
24
+ error: `HTTP error ${status}`,
25
+ details: String(detailText).substring(0, 500),
26
+ };
27
+ }
28
+ const err = new Error(errorData.error || errorData.reason || `HTTP Error: ${status} ${statusText}`);
29
+ err.details = errorData.details || errorData.detail || errorData.error || "No details provided.";
30
+ err.status = status;
31
+ throw err;
32
+ }
33
+
34
+ if (response.status === 204 || response.headers.get("Content-Length") === "0") {
35
+ return null;
36
+ }
37
+ return await response.json();
38
+ } catch (error) {
39
+ if (!error.details) error.details = error.message;
40
+ throw error;
41
+ }
42
+ }
43
+
44
+ static async downloadModel(params) {
45
+ return await this._request("/huggingface/download", {
46
+ method: "POST",
47
+ headers: { "Content-Type": "application/json" },
48
+ body: JSON.stringify(params),
49
+ });
50
+ }
51
+
52
+ static async getModelDetails(params) {
53
+ return await this._request("/huggingface/get_model_details", {
54
+ method: "POST",
55
+ headers: { "Content-Type": "application/json" },
56
+ body: JSON.stringify(params),
57
+ });
58
+ }
59
+
60
+ static async getStatus() {
61
+ return await this._request("/huggingface/status");
62
+ }
63
+
64
+ static async cancelDownload(downloadId) {
65
+ return await this._request("/huggingface/cancel", {
66
+ method: "POST",
67
+ headers: { "Content-Type": "application/json" },
68
+ body: JSON.stringify({ download_id: downloadId }),
69
+ });
70
+ }
71
+
72
+ static async searchModels(params) {
73
+ return await this._request("/huggingface/search", {
74
+ method: "POST",
75
+ headers: { "Content-Type": "application/json" },
76
+ body: JSON.stringify(params),
77
+ });
78
+ }
79
+
80
+ static async getBaseModels() {
81
+ return await this._request("/huggingface/base_models");
82
+ }
83
+
84
+ static async getModelTypes() {
85
+ return await this._request("/huggingface/model_types");
86
+ }
87
+
88
+ static async getModelDirs(modelType) {
89
+ const q = encodeURIComponent(modelType || 'checkpoints');
90
+ return await this._request(`/huggingface/model_dirs?type=${q}`);
91
+ }
92
+
93
+ static async createModelDir(modelType, newDir, root = "") {
94
+ return await this._request("/huggingface/create_dir", {
95
+ method: "POST",
96
+ headers: { "Content-Type": "application/json" },
97
+ body: JSON.stringify({ model_type: modelType, new_dir: newDir, root }),
98
+ });
99
+ }
100
+
101
+ static async createModelType(name) {
102
+ return await this._request("/huggingface/create_model_type", {
103
+ method: "POST",
104
+ headers: { "Content-Type": "application/json" },
105
+ body: JSON.stringify({ name }),
106
+ });
107
+ }
108
+
109
+ static async getModelRoots(modelType) {
110
+ const q = encodeURIComponent(modelType || 'checkpoints');
111
+ return await this._request(`/huggingface/model_roots?type=${q}`);
112
+ }
113
+
114
+ static async createModelRoot(modelType, absPath) {
115
+ return await this._request("/huggingface/create_root", {
116
+ method: "POST",
117
+ headers: { "Content-Type": "application/json" },
118
+ body: JSON.stringify({ model_type: modelType, path: absPath }),
119
+ });
120
+ }
121
+
122
+ static async getGlobalRoot() {
123
+ return await this._request("/huggingface/global_root");
124
+ }
125
+
126
+ static async setGlobalRoot(path) {
127
+ return await this._request("/huggingface/global_root", {
128
+ method: "POST",
129
+ headers: { "Content-Type": "application/json" },
130
+ body: JSON.stringify({ path }),
131
+ });
132
+ }
133
+
134
+ static async clearGlobalRoot() {
135
+ return await this._request("/huggingface/global_root/clear", {
136
+ method: "POST",
137
+ headers: { "Content-Type": "application/json" },
138
+ body: JSON.stringify({}),
139
+ });
140
+ }
141
+
142
+ static async retryDownload(downloadId) {
143
+ return await this._request("/huggingface/retry", {
144
+ method: "POST",
145
+ headers: { "Content-Type": "application/json" },
146
+ body: JSON.stringify({ download_id: downloadId }),
147
+ });
148
+ }
149
+
150
+ static async openPath(downloadId) {
151
+ return await this._request("/huggingface/open_path", {
152
+ method: "POST",
153
+ headers: { "Content-Type": "application/json" },
154
+ body: JSON.stringify({ download_id: downloadId }),
155
+ });
156
+ }
157
+
158
+ static async clearHistory() {
159
+ return await this._request("/huggingface/clear_history", {
160
+ method: "POST",
161
+ headers: { "Content-Type": "application/json" },
162
+ });
163
+ }
164
+ }
custom_nodes/ComfyUI-HuggingFace/web/js/huggingfaceDownloader.css ADDED
@@ -0,0 +1,753 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* HuggingFace Button Styling - Distinct from Civicomfy */
2
+ #huggingface-downloader-button {
3
+ background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%) !important;
4
+ color: #000 !important;
5
+ border: 2px solid #FF8C00 !important;
6
+ font-weight: bold !important;
7
+ padding: 4px 12px !important;
8
+ border-radius: 6px !important;
9
+ transition: all 0.3s ease !important;
10
+ }
11
+
12
+ #huggingface-downloader-button:hover {
13
+ background: linear-gradient(135deg, #FFA500 0%, #FF8C00 100%) !important;
14
+ transform: translateY(-1px) !important;
15
+ box-shadow: 0 4px 8px rgba(255, 140, 0, 0.3) !important;
16
+ }
17
+
18
+ #huggingface-downloader-button:active {
19
+ transform: translateY(0) !important;
20
+ box-shadow: 0 2px 4px rgba(255, 140, 0, 0.3) !important;
21
+ }
22
+
23
+ .huggingface-downloader-modal {
24
+ position: fixed;
25
+ z-index: 1001; /* Above ComfyUI elements */
26
+ left: 0;
27
+ top: 0;
28
+ width: 100%;
29
+ height: 100%;
30
+ overflow: hidden; /* Prevent body scroll */
31
+ background-color: rgba(0, 0, 0, 0.6);
32
+ display: flex;
33
+ justify-content: center;
34
+ align-items: center;
35
+ opacity: 0;
36
+ visibility: hidden;
37
+ transition: opacity 0.3s ease, visibility 0s linear 0.3s;
38
+ }
39
+
40
+ .huggingface-downloader-modal.open {
41
+ opacity: 1;
42
+ visibility: visible;
43
+ transition: opacity 0.3s ease, visibility 0s linear 0s;
44
+ }
45
+
46
+ .huggingface-downloader-modal-content {
47
+ background-color: var(--comfy-menu-bg);
48
+ color: var(--comfy-text-color);
49
+ margin: auto;
50
+ padding: 0; /* Remove padding, handle internally */
51
+ border-radius: 8px;
52
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
53
+ width: 900px;
54
+ max-width: 95%;
55
+ height: 700px; /* Fixed height */
56
+ max-height: 90vh;
57
+ display: flex; /* Use flexbox for layout */
58
+ flex-direction: column; /* Stack header, tabs, content */
59
+ overflow: hidden; /* Prevent content overflow */
60
+ }
61
+
62
+ .huggingface-downloader-header {
63
+ display: flex;
64
+ justify-content: space-between;
65
+ align-items: center;
66
+ padding: 15px 20px;
67
+ border-bottom: 1px solid var(--border-color, #444);
68
+ flex-shrink: 0; /* Prevent header from shrinking */
69
+ }
70
+
71
+ .huggingface-downloader-header h2 {
72
+ margin: 0;
73
+ font-size: 1.3em;
74
+ }
75
+
76
+ .huggingface-close-button {
77
+ background: none;
78
+ border: none;
79
+ color: var(--comfy-text-color);
80
+ font-size: 28px;
81
+ cursor: pointer;
82
+ padding: 0 5px;
83
+ line-height: 1;
84
+ }
85
+ .huggingface-close-button:hover {
86
+ color: #aaa;
87
+ }
88
+
89
+ .huggingface-downloader-body {
90
+ display: flex;
91
+ flex-direction: column;
92
+ flex-grow: 1; /* Allow body to take remaining space */
93
+ overflow: hidden; /* Manage internal scrolling */
94
+ }
95
+
96
+ .huggingface-downloader-tabs {
97
+ display: flex;
98
+ border-bottom: 1px solid var(--border-color, #444);
99
+ padding: 0 15px;
100
+ flex-shrink: 0; /* Prevent tabs from shrinking */
101
+ }
102
+
103
+ .huggingface-downloader-tab {
104
+ padding: 10px 18px;
105
+ cursor: pointer;
106
+ border: none;
107
+ background: none;
108
+ color: var(--comfy-text-color);
109
+ opacity: 0.7;
110
+ position: relative;
111
+ top: 1px; /* Align with bottom border */
112
+ margin-bottom: -1px; /* Overlap border */
113
+ }
114
+
115
+ .huggingface-downloader-tab.active {
116
+ opacity: 1;
117
+ border-bottom: 3px solid var(--accent-color, #5c8aff);
118
+ font-weight: bold;
119
+ }
120
+ .huggingface-downloader-tab:hover {
121
+ opacity: 1;
122
+ }
123
+
124
+ .huggingface-downloader-tab-content {
125
+ display: none;
126
+ padding: 20px;
127
+ flex-grow: 1; /* Allow content to take space */
128
+ overflow-y: auto; /* Enable scrolling ONLY for the content area */
129
+ }
130
+
131
+ .huggingface-downloader-tab-content.active {
132
+ display: block; /* Show active tab */
133
+ }
134
+
135
+ .huggingface-form-group {
136
+ margin-bottom: 18px;
137
+ }
138
+ .huggingface-form-group:last-child {
139
+ margin-bottom: 0;
140
+ }
141
+
142
+ .huggingface-form-group label {
143
+ display: block;
144
+ margin-bottom: 6px;
145
+ font-weight: bold;
146
+ font-size: 0.95em;
147
+ }
148
+
149
+ .huggingface-input,
150
+ .huggingface-select {
151
+ width: 100%;
152
+ padding: 10px;
153
+ background-color: var(--comfy-input-bg);
154
+ color: var(--comfy-text-color);
155
+ border: 1px solid var(--border-color, #555);
156
+ border-radius: 4px;
157
+ box-sizing: border-box; /* Include padding/border in width */
158
+ }
159
+ .huggingface-input:focus,
160
+ .huggingface-select:focus {
161
+ outline: none;
162
+ border-color: var(--accent-color, #5c8aff);
163
+ box-shadow: 0 0 0 2px rgba(92, 138, 255, 0.3);
164
+ }
165
+
166
+ .huggingface-form-row {
167
+ display: flex;
168
+ gap: 15px;
169
+ flex-wrap: wrap; /* Allow wrapping on smaller screens */
170
+ }
171
+ .huggingface-form-row > .huggingface-form-group {
172
+ flex: 1; /* Distribute space */
173
+ min-width: 180px; /* Minimum width before wrapping */
174
+ }
175
+ /* Adjust for checkbox/inline elements */
176
+ .huggingface-form-group.inline {
177
+ display: flex;
178
+ align-items: center;
179
+ gap: 8px;
180
+ margin-bottom: 10px; /* Less margin for checkboxes */
181
+ }
182
+ .huggingface-form-group.inline label {
183
+ margin-bottom: 0; /* Remove default bottom margin */
184
+ order: 1; /* Put label after checkbox */
185
+ }
186
+ .huggingface-checkbox {
187
+ width: auto;
188
+ order: 0; /* Put checkbox before label */
189
+ accent-color: var(--accent-color, #5c8aff);
190
+ transform: scale(1.1);
191
+ }
192
+
193
+ .huggingface-button {
194
+ background-color: var(--comfy-input-bg);
195
+ color: var(--comfy-text-color);
196
+ border: 1px solid var(--border-color, #555);
197
+ padding: 10px 18px;
198
+ border-radius: 4px;
199
+ cursor: pointer;
200
+ font-size: 1em;
201
+ transition: background-color 0.2s ease, border-color 0.2s ease;
202
+ }
203
+ .huggingface-button:hover {
204
+ background-color: var(--comfy-input-bg-hover);
205
+ border-color: #777;
206
+ }
207
+ .huggingface-button:disabled {
208
+ opacity: 0.5;
209
+ cursor: not-allowed;
210
+ }
211
+
212
+ .huggingface-button.primary {
213
+ background-color: var(--accent-color, #5c8aff);
214
+ border-color: var(--accent-color, #5c8aff);
215
+ color: white;
216
+ }
217
+ .huggingface-button.primary:hover {
218
+ background-color: #4a7ee0; /* Slightly darker blue */
219
+ border-color: #4a7ee0;
220
+ }
221
+ .huggingface-button.primary:disabled {
222
+ background-color: var(--accent-color, #5c8aff); /* Keep color but lower opacity */
223
+ border-color: var(--accent-color, #5c8aff);
224
+ }
225
+
226
+ .huggingface-button.danger {
227
+ background-color: #e64b4b;
228
+ border-color: #e64b4b;
229
+ color: white;
230
+ }
231
+ .huggingface-button.danger:hover {
232
+ background-color: #d93a3a;
233
+ border-color: #d93a3a;
234
+ }
235
+ .huggingface-button.small {
236
+ padding: 5px 10px;
237
+ font-size: 0.85em;
238
+ }
239
+
240
+ /* Status Tab Styling */
241
+ .huggingface-status-section {
242
+ margin-bottom: 25px;
243
+ }
244
+ .huggingface-status-section h3 {
245
+ margin-top: 0;
246
+ margin-bottom: 15px;
247
+ border-bottom: 1px solid var(--border-color, #444);
248
+ padding-bottom: 8px;
249
+ font-size: 1.1em;
250
+ }
251
+
252
+ .huggingface-download-list {
253
+ display: flex;
254
+ flex-direction: column;
255
+ gap: 15px;
256
+ }
257
+
258
+ .huggingface-download-item {
259
+ display: flex;
260
+ gap: 15px;
261
+ align-items: flex-start; /* Align items top */
262
+ padding: 15px;
263
+ border-radius: 6px;
264
+ background-color: var(--comfy-input-bg); /* Slightly different bg for items */
265
+ border: 1px solid var(--border-color, #555);
266
+ }
267
+
268
+ .huggingface-download-thumbnail {
269
+ width: 80px;
270
+ height: 80px;
271
+ object-fit: cover;
272
+ border-radius: 4px;
273
+ background-color: #333; /* Placeholder color */
274
+ flex-shrink: 0; /* Don't shrink thumbnail */
275
+ margin-top: 3px; /* Align better with text */
276
+ }
277
+
278
+ .huggingface-download-info {
279
+ flex-grow: 1; /* Allow info to take available space */
280
+ display: flex;
281
+ flex-direction: column;
282
+ gap: 5px;
283
+ overflow: hidden; /* Prevent long text overflow */
284
+ }
285
+ .huggingface-download-info strong {
286
+ font-size: 1.05em;
287
+ white-space: nowrap;
288
+ overflow: hidden;
289
+ text-overflow: ellipsis;
290
+ }
291
+ .huggingface-download-info p {
292
+ margin: 0;
293
+ font-size: 0.9em;
294
+ color: #ccc;
295
+ white-space: nowrap;
296
+ overflow: hidden;
297
+ text-overflow: ellipsis;
298
+ }
299
+ .huggingface-download-info .filename {
300
+ font-style: italic;
301
+ color: #bbb;
302
+ }
303
+ .huggingface-download-info .error-message {
304
+ color: #ff6b6b;
305
+ font-size: 0.85em;
306
+ white-space: normal; /* Allow error message to wrap */
307
+ word-break: break-word;
308
+ }
309
+
310
+ .huggingface-download-actions {
311
+ display: flex;
312
+ align-items: center;
313
+ gap: 8px;
314
+ margin-left: auto; /* Push actions to the right */
315
+ flex-shrink: 0; /* Don't shrink action buttons */
316
+ }
317
+
318
+ .huggingface-progress-container {
319
+ margin-top: 8px;
320
+ width: 100%;
321
+ background-color: var(--comfy-menu-bg); /* Darker background for contrast */
322
+ border-radius: 4px;
323
+ overflow: hidden;
324
+ height: 20px;
325
+ border: 1px solid var(--border-color, #555);
326
+ }
327
+
328
+ .huggingface-progress-bar {
329
+ height: 100%;
330
+ background-color: var(--accent-color, #5c8aff);
331
+ text-align: center;
332
+ color: white;
333
+ line-height: 20px; /* Center text vertically */
334
+ font-size: 12px;
335
+ font-weight: bold;
336
+ transition: width 0.3s ease-out;
337
+ min-width: 20px; /* Show something even for 0% */
338
+ width: 0%; /* Start at 0 */
339
+ white-space: nowrap;
340
+ overflow: hidden;
341
+ }
342
+ .huggingface-progress-bar.completed {
343
+ background-color: #4caf50; /* Green for completed */
344
+ }
345
+ .huggingface-progress-bar.failed {
346
+ background-color: #f44336; /* Red for failed */
347
+ }
348
+ .huggingface-progress-bar.cancelled {
349
+ background-color: #9e9e9e; /* Grey for cancelled */
350
+ }
351
+
352
+ .huggingface-speed-indicator {
353
+ font-size: 0.85em;
354
+ color: #ccc;
355
+ margin-top: 4px;
356
+ text-align: right;
357
+ }
358
+
359
+ /* Search Tab Styling */
360
+ .huggingface-search-controls {
361
+ display: flex;
362
+ gap: 15px;
363
+ margin-bottom: 20px;
364
+ flex-wrap: wrap;
365
+ }
366
+ .huggingface-search-controls .huggingface-input {
367
+ flex-grow: 1; /* Let search input take more space */
368
+ }
369
+ .huggingface-search-controls .huggingface-select {
370
+ min-width: 150px; /* Min width for dropdowns */
371
+ }
372
+ .huggingface-search-controls .huggingface-button {
373
+ align-self: flex-end; /* Align button with bottom of inputs */
374
+ }
375
+
376
+ .huggingface-search-results {
377
+ margin-top: 20px;
378
+ }
379
+ .huggingface-search-item {
380
+ display: flex;
381
+ gap: 15px;
382
+ align-items: flex-start;
383
+ margin-bottom: 15px;
384
+ padding: 15px;
385
+ border-radius: 6px;
386
+ background-color: var(--comfy-input-bg);
387
+ border: 1px solid var(--border-color, #555);
388
+ transition: background-color 0.2s ease;
389
+ }
390
+
391
+ .huggingface-thumbnail-container {
392
+ position: relative; /* Required for absolute positioning of the child badge */
393
+ display: block;
394
+ line-height: 0;
395
+ width: 120px;
396
+ height: 170px;
397
+ flex-shrink: 0;
398
+ }
399
+
400
+ .huggingface-search-thumbnail {
401
+ width: 120px; /* Slightly larger for search */
402
+ height: 170px;
403
+ object-fit: cover;
404
+ border-radius: 4px;
405
+ background-color: #333;
406
+ flex-shrink: 0;
407
+ }
408
+
409
+ .huggingface-search-meta-info {
410
+ font-size: 0.85em;
411
+ color: #aaa;
412
+ margin-bottom: 5px;
413
+ display: flex;
414
+ gap: 15px;
415
+ flex-wrap: wrap;
416
+ }
417
+
418
+ .huggingface-search-meta-info span {
419
+ display: inline-flex;
420
+ align-items: center;
421
+ gap: 5px;
422
+ }
423
+
424
+ .huggingface-search-meta-info i {
425
+ color: #ccc;
426
+ }
427
+ .huggingface-type-badge {
428
+ position: absolute;
429
+ bottom: 5px; /* Adjust spacing from bottom */
430
+ left: 5px; /* Adjust spacing from left */
431
+ background-color: rgb(48, 94, 70); /* Black background with transparency */
432
+ color: white;
433
+ padding: 10px 8px; /* Adjust padding (top/bottom, left/right) */
434
+ border-radius: 10px; /* Adjust for desired roundness */
435
+ font-size: 0.75em; /* Adjust font size */
436
+ font-weight: bold;
437
+ z-index: 2; /* Ensure it's above the image */
438
+ white-space: nowrap; /* Prevent text wrapping */
439
+ }
440
+
441
+ .huggingface-search-info {
442
+ flex-grow: 1;
443
+ display: flex;
444
+ flex-direction: column;
445
+ gap: 5px;
446
+ overflow: hidden;
447
+ }
448
+ .huggingface-search-info h4 {
449
+ margin: 0;
450
+ font-size: 1.1em;
451
+ white-space: nowrap;
452
+ overflow: hidden;
453
+ text-overflow: ellipsis;
454
+ }
455
+ .huggingface-search-info p {
456
+ margin: 0;
457
+ font-size: 0.9em;
458
+ color: #ccc;
459
+ }
460
+ .huggingface-search-tags {
461
+ display: flex;
462
+ flex-wrap: wrap;
463
+ gap: 5px;
464
+ margin-top: 8px;
465
+ }
466
+ .huggingface-search-tag {
467
+ background-color: rgba(255, 255, 255, 0.1);
468
+ color: #eee;
469
+ padding: 3px 8px;
470
+ border-radius: 10px;
471
+ font-size: 0.8em;
472
+ }
473
+ .huggingface-search-stats span {
474
+ margin-right: 15px;
475
+ font-size: 0.85em;
476
+ color: #bbb;
477
+ }
478
+ .huggingface-search-stats i { /* If using icons */
479
+ margin-right: 4px;
480
+ }
481
+
482
+ .huggingface-search-actions {
483
+ display: flex;
484
+ flex-direction: column; /* Stack buttons vertically */
485
+ gap: 8px;
486
+ align-items: flex-end; /* Align buttons to the right */
487
+ margin-left: auto;
488
+ flex-shrink: 0;
489
+ }
490
+
491
+ .version-buttons-container {
492
+ display: flex;
493
+ flex-direction: column;
494
+ align-items: flex-end;
495
+ gap: 5px;
496
+ }
497
+
498
+ .all-versions-container {
499
+ display: flex;
500
+ flex-direction: column;
501
+ gap: 5px;
502
+ padding-bottom: 5px;
503
+ white-space: nowrap; /* Prevent wrapping */
504
+ max-width: 100%; /* Prevent excessive width */
505
+ align-items: flex-end;
506
+ }
507
+
508
+ .show-all-versions-button {
509
+ align-self: flex-end; /* Align button to left */
510
+ margin-top: auto; /* Push to the bottom */
511
+ }
512
+ .base-model-badge {
513
+ display: inline-block; /* Allows padding and background */
514
+ background-color: #3a3a3a; /* Or a specific purple hex code like #800080 */
515
+ color: white;
516
+ font-weight: bold;
517
+ padding: 2px 6px; /* Adjust padding for desired size (vertical horizontal) */
518
+ border-radius: 3px; /* Adjust for desired roundness */
519
+ margin-right: 4px; /* Optional: Adds some space between badge and hyphen */
520
+ line-height: 1; /* Optional: Can help contain the background better */
521
+ vertical-align: middle; /* Optional: Helps align the badge nicely with text */
522
+ }
523
+
524
+
525
+ /* Settings Tab Styling */
526
+ .huggingface-settings-container {
527
+ display: grid;
528
+ grid-template-columns: 1fr; /* Single column */
529
+ gap: 20px;
530
+ }
531
+
532
+ @media (min-width: 768px) {
533
+ .huggingface-settings-container {
534
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); /* Responsive columns */
535
+ }
536
+ }
537
+
538
+ .huggingface-settings-section {
539
+ background-color: var(--comfy-input-bg);
540
+ padding: 15px;
541
+ border-radius: 6px;
542
+ border: 1px solid var(--border-color, #555);
543
+ }
544
+ .huggingface-settings-section h4 {
545
+ margin-top: 0;
546
+ margin-bottom: 15px;
547
+ border-bottom: 1px solid var(--border-color, #444);
548
+ padding-bottom: 8px;
549
+ }
550
+
551
+ .huggingface-toast {
552
+ position: fixed;
553
+ bottom: 20px;
554
+ left: 50%;
555
+ transform: translateX(-50%);
556
+ background-color: rgba(0, 0, 0, 0.8);
557
+ color: white;
558
+ padding: 12px 20px;
559
+ border-radius: 6px;
560
+ z-index: 1005; /* Above modal */
561
+ font-size: 0.95em;
562
+ opacity: 0;
563
+ transition: opacity 0.3s ease, bottom 0.3s ease;
564
+ pointer-events: none; /* Don't block clicks */
565
+ }
566
+
567
+ .huggingface-toast.show {
568
+ opacity: 1;
569
+ bottom: 30px;
570
+ }
571
+
572
+ .huggingface-toast.success {
573
+ background-color: rgba(76, 175, 80, 0.9); /* Green */
574
+ }
575
+
576
+ .huggingface-toast.error {
577
+ background-color: rgba(244, 67, 54, 0.9); /* Red */
578
+ }
579
+
580
+ .huggingface-toast.info {
581
+ background-color: rgba(33, 150, 243, 0.9); /* Blue */
582
+ }
583
+
584
+ /* Tooltip Styles */
585
+ [data-tooltip] {
586
+ position: relative;
587
+ cursor: help;
588
+ }
589
+ [data-tooltip]::after {
590
+ content: attr(data-tooltip);
591
+ position: absolute;
592
+ left: 50%;
593
+ transform: translateX(-50%);
594
+ bottom: 100%;
595
+ margin-bottom: 5px;
596
+ background-color: rgba(0, 0, 0, 0.85);
597
+ color: white;
598
+ padding: 5px 10px;
599
+ border-radius: 4px;
600
+ font-size: 0.85em;
601
+ white-space: nowrap;
602
+ opacity: 0;
603
+ visibility: hidden;
604
+ transition: opacity 0.2s ease, visibility 0.2s ease;
605
+ z-index: 1010; /* Ensure tooltip is on top */
606
+ }
607
+ [data-tooltip]:hover::after {
608
+ opacity: 1;
609
+ visibility: visible;
610
+ }
611
+
612
+ .huggingface-download-preview-area {
613
+ /* Add specific margins/padding if needed */
614
+ min-height: 50px; /* Ensure it has some height for the loading spinner */
615
+ }
616
+
617
+ /* Style for the description box */
618
+ .model-description-content {
619
+ max-height: 200px; /* Limit height */
620
+ overflow-y: auto; /* Allow scrolling */
621
+ background-color: var(--comfy-input-bg); /* Match input background */
622
+ padding: 10px;
623
+ border-radius: 4px;
624
+ font-size: 0.9em; /* Slightly smaller text */
625
+ border: 1px solid var(--border-color, #555);
626
+ color: var(--comfy-text-color); /* Ensure text color matches */
627
+ line-height: 1.5; /* Improve readability */
628
+ }
629
+
630
+ /* Style links within the description */
631
+ .model-description-content a {
632
+ color: var(--accent-color, #5c8aff); /* Make links stand out */
633
+ text-decoration: underline;
634
+ }
635
+ .model-description-content a:hover {
636
+ color: #8bb0ff; /* Lighter color on hover */
637
+ }
638
+
639
+ /* Ensure reused search item styles look okay in this context */
640
+ .huggingface-download-preview-area .huggingface-search-item {
641
+ margin-bottom: 15px; /* Add space below the item */
642
+ background-color: var(--comfy-input-bg); /* Explicitly set background */
643
+ }
644
+
645
+ /* --- Confirmation Modal Styles --- */
646
+ .huggingface-confirmation-modal {
647
+ display: none; /* Hidden by default */
648
+ position: fixed; /* Stay in place */
649
+ z-index: 1001; /* Ensure it's above the main modal */
650
+ left: 0;
651
+ top: 0;
652
+ width: 100%;
653
+ height: 100%;
654
+ overflow: auto; /* Enable scroll if needed */
655
+ background-color: rgba(0,0,0,0.6); /* Dim background */
656
+ /* Use flexbox for easy centering */
657
+ justify-content: center;
658
+ align-items: center;
659
+ }
660
+
661
+ .huggingface-confirmation-modal-content {
662
+ background-color: var(--comfy-menu-bg, #282828);
663
+ margin: auto;
664
+ padding: 25px;
665
+ border: 1px solid var(--border-color, #444);
666
+ border-radius: 8px;
667
+ width: 80%;
668
+ max-width: 450px; /* Limit width */
669
+ box-shadow: 0 5px 15px rgba(0,0,0,0.3);
670
+ color: var(--input-text, #ddd);
671
+ }
672
+
673
+ .huggingface-confirmation-modal-content h4 {
674
+ margin-top: 0;
675
+ margin-bottom: 15px;
676
+ color: var(--desc-text, #eee);
677
+ border-bottom: 1px solid var(--border-color, #444);
678
+ padding-bottom: 10px;
679
+ }
680
+
681
+ .huggingface-confirmation-modal-content p {
682
+ margin-bottom: 25px;
683
+ line-height: 1.5;
684
+ font-size: 0.95em;
685
+ }
686
+
687
+ .huggingface-confirmation-modal-actions {
688
+ display: flex;
689
+ justify-content: flex-end; /* Align buttons to the right */
690
+ gap: 10px; /* Space between buttons */
691
+ }
692
+
693
+ /* Style buttons within the confirmation modal */
694
+ .huggingface-confirmation-modal-actions .huggingface-button {
695
+ padding: 8px 15px; /* Adjust padding */
696
+ min-width: 80px; /* Ensure minimum width */
697
+ }
698
+
699
+ /* Ensure both img and video within the thumbnail container behave similarly */
700
+ .huggingface-thumbnail-container img.huggingface-search-thumbnail,
701
+ .huggingface-thumbnail-container video.huggingface-search-thumbnail,
702
+ .huggingface-thumbnail-container img.huggingface-download-thumbnail,
703
+ .huggingface-thumbnail-container video.huggingface-download-thumbnail {
704
+ display: block; /* Prevent extra space below */
705
+ width: 100%; /* Fill the container width */
706
+ height: 100%; /* Fill the container height */
707
+ object-fit: cover; /* Scale while maintaining aspect ratio, cropping if needed */
708
+ background-color: #333; /* Background for loading/error states */
709
+ }
710
+
711
+ /* You might already have rules for the container itself, ensure they define a size */
712
+ .huggingface-thumbnail-container {
713
+ width: 100px; /* Example size */
714
+ height: 140px; /* Example size */
715
+ overflow: hidden; /* Hide parts of the video/image outside the cover area */
716
+ position: relative; /* For positioning badges etc. */
717
+ flex-shrink: 0; /* Prevent shrinking in flex layouts */
718
+ border-radius: 4px; /* Match styling */
719
+ }
720
+
721
+ /* Blur/Hide for R-rated thumbnails */
722
+ .huggingface-thumbnail-container.blurred img.huggingface-search-thumbnail,
723
+ .huggingface-thumbnail-container.blurred video.huggingface-search-thumbnail,
724
+ .huggingface-thumbnail-container.blurred img.huggingface-download-thumbnail,
725
+ .huggingface-thumbnail-container.blurred video.huggingface-download-thumbnail {
726
+ filter: blur(14px) brightness(0.6) saturate(0.6);
727
+ pointer-events: none; /* Let clicks hit the container */
728
+ }
729
+ .huggingface-thumbnail-container .huggingface-nsfw-overlay {
730
+ position: absolute;
731
+ inset: 0;
732
+ display: flex;
733
+ align-items: center;
734
+ justify-content: center;
735
+ color: #fff;
736
+ font-weight: 800;
737
+ font-size: 20px;
738
+ letter-spacing: 1px;
739
+ background: rgba(0,0,0,0.35);
740
+ z-index: 3;
741
+ user-select: none;
742
+ }
743
+ /* Make it obvious it’s clickable */
744
+ .huggingface-thumbnail-container.blurred { cursor: pointer; }
745
+
746
+ /* Style for the type badge (likely exists) */
747
+ .huggingface-type-badge {
748
+ /* ... existing styles ... */
749
+ position: absolute;
750
+ bottom: 5px;
751
+ right: 5px;
752
+ /* ... */
753
+ }
custom_nodes/ComfyUI-HuggingFace/web/js/huggingfaceDownloader.js ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { app } from "../../../scripts/app.js";
2
+ import { addCssLink } from "./utils/dom.js";
3
+ import { HuggingFaceDownloaderUI } from "./ui/UI.js";
4
+
5
+ console.log("Loading HuggingFace UI...");
6
+
7
+ // --- Configuration ---
8
+ const EXTENSION_NAME = "HuggingFace";
9
+ const CSS_URL = `../huggingfaceDownloader.css`;
10
+ const PLACEHOLDER_IMAGE_URL = `/extensions/ComfyUI-HuggingFace/images/placeholder.jpg`;
11
+
12
+ // Add Menu Button to ComfyUI
13
+ function addMenuButton() {
14
+ const buttonGroup = document.querySelector(".comfyui-button-group");
15
+
16
+ if (!buttonGroup) {
17
+ console.warn(`[${EXTENSION_NAME}] ComfyUI button group not found. Retrying...`);
18
+ setTimeout(addMenuButton, 500);
19
+ return;
20
+ }
21
+
22
+ if (document.getElementById("huggingface-downloader-button")) {
23
+ console.log(`[${EXTENSION_NAME}] Button already exists.`);
24
+ return;
25
+ }
26
+
27
+ const huggingfaceButton = document.createElement("button");
28
+ huggingfaceButton.innerHTML = "🤗 HuggingFace";
29
+ huggingfaceButton.id = "huggingface-downloader-button";
30
+ huggingfaceButton.title = "Open HuggingFace Model Downloader";
31
+
32
+ huggingfaceButton.onclick = async () => {
33
+ if (!window.huggingfaceDownloaderUI) {
34
+ console.info(`[${EXTENSION_NAME}] Creating HuggingFaceDownloaderUI instance...`);
35
+ window.huggingfaceDownloaderUI = new HuggingFaceDownloaderUI();
36
+ document.body.appendChild(window.huggingfaceDownloaderUI.modal);
37
+
38
+ try {
39
+ await window.huggingfaceDownloaderUI.initializeUI();
40
+ console.info(`[${EXTENSION_NAME}] UI Initialization complete.`);
41
+ } catch (error) {
42
+ console.error(`[${EXTENSION_NAME}] Error during UI initialization:`, error);
43
+ window.huggingfaceDownloaderUI?.showToast("Error initializing UI components. Check console.", "error", 5000);
44
+ }
45
+ }
46
+
47
+ if (window.huggingfaceDownloaderUI) {
48
+ window.huggingfaceDownloaderUI.openModal();
49
+ } else {
50
+ console.error(`[${EXTENSION_NAME}] Cannot open modal: UI instance not available.`);
51
+ alert("HuggingFace failed to initialize. Please check the browser console for errors.");
52
+ }
53
+ };
54
+
55
+ buttonGroup.appendChild(huggingfaceButton);
56
+ console.log(`[${EXTENSION_NAME}] HuggingFace button added to .comfyui-button-group.`);
57
+
58
+ const menu = document.querySelector(".comfy-menu");
59
+ if (!buttonGroup.contains(huggingfaceButton) && menu && !menu.contains(huggingfaceButton)) {
60
+ console.warn(`[${EXTENSION_NAME}] Failed to append button to group, falling back to menu.`);
61
+ const settingsButton = menu.querySelector("#comfy-settings-button");
62
+ if (settingsButton) {
63
+ settingsButton.insertAdjacentElement("beforebegin", huggingfaceButton);
64
+ } else {
65
+ menu.appendChild(huggingfaceButton);
66
+ }
67
+ }
68
+ }
69
+
70
+ // --- Initialization ---
71
+ app.registerExtension({
72
+ name: "ComfyUI-HuggingFace.HuggingFaceDownloader",
73
+ async setup(appInstance) {
74
+ console.log(`[${EXTENSION_NAME}] Setting up HuggingFace Extension...`);
75
+ addCssLink(CSS_URL);
76
+ addMenuButton();
77
+
78
+ // Optional: Pre-check placeholder image
79
+ fetch(PLACEHOLDER_IMAGE_URL)
80
+ .then(res => {
81
+ if (!res.ok) {
82
+ console.warn(`[${EXTENSION_NAME}] Placeholder image not found at ${PLACEHOLDER_IMAGE_URL}.`);
83
+ }
84
+ })
85
+ .catch(err => console.warn(`[${EXTENSION_NAME}] Error checking for placeholder image:`, err));
86
+
87
+ console.log(`[${EXTENSION_NAME}] Extension setup complete. UI will initialize on first click.`);
88
+ },
89
+ });
custom_nodes/ComfyUI-HuggingFace/web/js/ui/UI.js ADDED
@@ -0,0 +1,465 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Feedback } from "./feedback.js";
2
+ import { setupEventListeners } from "./handlers/eventListeners.js";
3
+ import { handleDownloadSubmit, fetchAndDisplayDownloadPreview, debounceFetchDownloadPreview } from "./handlers/downloadHandler.js";
4
+ import { handleSearchSubmit } from "./handlers/searchHandler.js";
5
+ import {
6
+ handleSettingsSave,
7
+ loadAndApplySettings,
8
+ loadSettingsFromCookie,
9
+ saveSettingsToCookie,
10
+ applySettings,
11
+ getDefaultSettings,
12
+ loadGlobalRootSetting,
13
+ handleSetGlobalRoot,
14
+ handleClearGlobalRoot,
15
+ } from "./handlers/settingsHandler.js";
16
+ import { startStatusUpdates, stopStatusUpdates, updateStatus, handleCancelDownload, handleRetryDownload, handleOpenPath, handleClearHistory } from "./handlers/statusHandler.js";
17
+ import { renderSearchResults } from "./searchRenderer.js";
18
+ import { renderDownloadList } from "./statusRenderer.js";
19
+ import { renderDownloadPreview } from "./previewRenderer.js";
20
+ import { modalTemplate } from "./templates.js";
21
+ import { HuggingFaceDownloaderAPI } from "../api/huggingface.js";
22
+
23
+ export class HuggingFaceDownloaderUI {
24
+ constructor() {
25
+ this.modal = null;
26
+ this.tabs = {};
27
+ this.tabContents = {};
28
+ this.activeTab = 'download';
29
+ this.modelTypes = {};
30
+ this.statusInterval = null;
31
+ this.statusData = { queue: [], active: [], history: [] };
32
+ this.baseModels = [];
33
+ this.searchPagination = { currentPage: 1, totalPages: 1, limit: 20 };
34
+ this.settings = this.getDefaultSettings();
35
+ this.toastTimeout = null;
36
+ this.modelPreviewDebounceTimeout = null;
37
+
38
+ this.updateStatus();
39
+ this.buildModalHTML();
40
+ this.cacheDOMElements();
41
+ this.setupEventListeners();
42
+ this.feedback = new Feedback(this.modal.querySelector('#huggingface-toast'));
43
+ // Ensure icon stylesheet is loaded so buttons render icons immediately
44
+ this.ensureFontAwesome();
45
+ }
46
+
47
+ // --- Core UI Methods ---
48
+ buildModalHTML() {
49
+ this.modal = document.createElement('div');
50
+ this.modal.className = 'huggingface-downloader-modal';
51
+ this.modal.id = 'huggingface-downloader-modal';
52
+ this.modal.innerHTML = modalTemplate(this.settings);
53
+ }
54
+
55
+ cacheDOMElements() {
56
+ this.closeButton = this.modal.querySelector('#huggingface-close-modal');
57
+ this.tabContainer = this.modal.querySelector('.huggingface-downloader-tabs');
58
+
59
+ // Download Tab
60
+ this.downloadForm = this.modal.querySelector('#huggingface-download-form');
61
+ this.downloadPreviewArea = this.modal.querySelector('#huggingface-download-preview-area');
62
+ this.modelUrlInput = this.modal.querySelector('#huggingface-model-url');
63
+ this.modelVersionIdInput = this.modal.querySelector('#huggingface-model-version-id');
64
+ this.downloadModelTypeSelect = this.modal.querySelector('#huggingface-model-type');
65
+ this.createModelTypeButton = this.modal.querySelector('#huggingface-create-model-type');
66
+ this.customFilenameInput = this.modal.querySelector('#huggingface-custom-filename');
67
+ this.subdirSelect = this.modal.querySelector('#huggingface-subdir-select');
68
+ this.createSubdirButton = this.modal.querySelector('#huggingface-create-subdir');
69
+ this.saveBasePathHint = this.modal.querySelector('#huggingface-save-base-path');
70
+ this.downloadConnectionsInput = this.modal.querySelector('#huggingface-connections');
71
+ this.forceRedownloadCheckbox = this.modal.querySelector('#huggingface-force-redownload');
72
+ this.downloadSubmitButton = this.modal.querySelector('#huggingface-download-submit');
73
+
74
+ // Search Tab
75
+ this.searchForm = this.modal.querySelector('#huggingface-search-form');
76
+ this.searchQueryInput = this.modal.querySelector('#huggingface-search-query');
77
+ this.searchTypeSelect = this.modal.querySelector('#huggingface-search-type');
78
+ this.searchBaseModelSelect = this.modal.querySelector('#huggingface-search-base-model');
79
+ this.searchSortSelect = this.modal.querySelector('#huggingface-search-sort');
80
+ this.searchPeriodSelect = this.modal.querySelector('#huggingface-search-period');
81
+ this.searchSubmitButton = this.modal.querySelector('#huggingface-search-submit');
82
+ this.searchResultsContainer = this.modal.querySelector('#huggingface-search-results');
83
+ this.searchPaginationContainer = this.modal.querySelector('#huggingface-search-pagination');
84
+
85
+ // Status Tab
86
+ this.statusContent = this.modal.querySelector('#huggingface-status-content');
87
+ this.activeListContainer = this.modal.querySelector('#huggingface-active-list');
88
+ this.queuedListContainer = this.modal.querySelector('#huggingface-queued-list');
89
+ this.historyListContainer = this.modal.querySelector('#huggingface-history-list');
90
+ this.statusIndicator = this.modal.querySelector('#huggingface-status-indicator');
91
+ this.activeCountSpan = this.modal.querySelector('#huggingface-active-count');
92
+ this.clearHistoryButton = this.modal.querySelector('#huggingface-clear-history-button');
93
+ this.confirmClearModal = this.modal.querySelector('#huggingface-confirm-clear-modal');
94
+ this.confirmClearYesButton = this.modal.querySelector('#huggingface-confirm-clear-yes');
95
+ this.confirmClearNoButton = this.modal.querySelector('#huggingface-confirm-clear-no');
96
+
97
+ // Settings Tab
98
+ this.settingsForm = this.modal.querySelector('#huggingface-settings-form');
99
+ this.settingsApiKeyInput = this.modal.querySelector('#huggingface-settings-api-key');
100
+ this.settingsGlobalRootInput = this.modal.querySelector('#huggingface-settings-global-root');
101
+ this.settingsSetGlobalRootButton = this.modal.querySelector('#huggingface-settings-set-global-root');
102
+ this.settingsClearGlobalRootButton = this.modal.querySelector('#huggingface-settings-clear-global-root');
103
+ this.settingsConnectionsInput = this.modal.querySelector('#huggingface-settings-connections');
104
+ this.settingsDefaultTypeSelect = this.modal.querySelector('#huggingface-settings-default-type');
105
+ this.settingsAutoOpenCheckbox = this.modal.querySelector('#huggingface-settings-auto-open-status');
106
+ this.settingsHideMatureCheckbox = this.modal.querySelector('#huggingface-settings-hide-mature');
107
+ this.settingsNsfwThresholdInput = this.modal.querySelector('#huggingface-settings-nsfw-threshold');
108
+ this.settingsSaveButton = this.modal.querySelector('#huggingface-settings-save');
109
+
110
+ // Toast Notification
111
+ this.toastElement = this.modal.querySelector('#huggingface-toast');
112
+
113
+ // Collect tabs and contents
114
+ this.tabs = {};
115
+ this.modal.querySelectorAll('.huggingface-downloader-tab').forEach(tab => {
116
+ this.tabs[tab.dataset.tab] = tab;
117
+ });
118
+ this.tabContents = {};
119
+ this.modal.querySelectorAll('.huggingface-downloader-tab-content').forEach(content => {
120
+ const tabName = content.id.replace('huggingface-tab-', '');
121
+ if (tabName) this.tabContents[tabName] = content;
122
+ });
123
+ }
124
+
125
+ async initializeUI() {
126
+ console.info("[HuggingFace] Initializing UI components...");
127
+ await this.populateModelTypes();
128
+ await this.populateBaseModels();
129
+ this.loadAndApplySettings();
130
+ await this.loadGlobalRootSetting();
131
+ if (this.downloadModelTypeSelect) {
132
+ await this.loadAndPopulateSubdirs(this.downloadModelTypeSelect.value);
133
+ }
134
+ }
135
+
136
+ async populateModelTypes() {
137
+ console.log("[HuggingFace] Populating model types...");
138
+ try {
139
+ const types = await HuggingFaceDownloaderAPI.getModelTypes();
140
+ if (!types || typeof types !== 'object' || Object.keys(types).length === 0) {
141
+ throw new Error("Received invalid model types data format.");
142
+ }
143
+ this.modelTypes = types;
144
+ const sortedTypes = Object.entries(this.modelTypes).sort((a, b) => a[1].localeCompare(b[1]));
145
+
146
+ this.downloadModelTypeSelect.innerHTML = '';
147
+ this.searchTypeSelect.innerHTML = '<option value="any">Any Type</option>';
148
+ this.settingsDefaultTypeSelect.innerHTML = '';
149
+
150
+ sortedTypes.forEach(([key, displayName]) => {
151
+ const option = document.createElement('option');
152
+ option.value = key;
153
+ option.textContent = displayName;
154
+ this.downloadModelTypeSelect.appendChild(option.cloneNode(true));
155
+ this.settingsDefaultTypeSelect.appendChild(option.cloneNode(true));
156
+ this.searchTypeSelect.appendChild(option.cloneNode(true));
157
+ });
158
+ // After types are populated, load subdirs for the current selection
159
+ await this.loadAndPopulateSubdirs(this.downloadModelTypeSelect.value);
160
+ } catch (error) {
161
+ console.error("[HuggingFace] Failed to get or populate model types:", error);
162
+ this.showToast('Failed to load model types', 'error');
163
+ this.downloadModelTypeSelect.innerHTML = '<option value="checkpoints">Checkpoints (Default)</option>';
164
+ this.modelTypes = { "checkpoints": "Checkpoints (Default)" };
165
+ }
166
+ }
167
+
168
+ async loadAndPopulateSubdirs(modelType) {
169
+ try {
170
+ const res = await HuggingFaceDownloaderAPI.getModelDirs(modelType);
171
+ const select = this.subdirSelect;
172
+ if (!select) return;
173
+ const current = select.value;
174
+ select.innerHTML = '';
175
+ const optRoot = document.createElement('option');
176
+ optRoot.value = '';
177
+ optRoot.textContent = '(root)';
178
+ select.appendChild(optRoot);
179
+ if (res && Array.isArray(res.subdirs)) {
180
+ // res.subdirs contains '' for root; skip empty since we added (root)
181
+ res.subdirs.filter(p => p && typeof p === 'string').forEach(rel => {
182
+ const opt = document.createElement('option');
183
+ opt.value = rel;
184
+ opt.textContent = rel;
185
+ select.appendChild(opt);
186
+ });
187
+ }
188
+ // Restore selection if still present
189
+ if (Array.from(select.options).some(o => o.value === current)) {
190
+ select.value = current;
191
+ }
192
+ if (this.saveBasePathHint) {
193
+ const basePath = (res && typeof res.base_dir === 'string') ? res.base_dir : '';
194
+ this.saveBasePathHint.textContent = basePath ? `Base path: ${basePath}` : '';
195
+ }
196
+ } catch (e) {
197
+ console.error('[HuggingFace] Failed to load subdirectories:', e);
198
+ if (this.subdirSelect) {
199
+ this.subdirSelect.innerHTML = '<option value="">(root)</option>';
200
+ }
201
+ if (this.saveBasePathHint) {
202
+ this.saveBasePathHint.textContent = '';
203
+ }
204
+ }
205
+ }
206
+
207
+ // (loadAndPopulateRoots removed; dynamic types already reflect models/ subfolders)
208
+
209
+ async populateBaseModels() {
210
+ console.log("[HuggingFace] Populating base models...");
211
+ try {
212
+ const result = await HuggingFaceDownloaderAPI.getBaseModels();
213
+ if (!result || !Array.isArray(result.base_models)) {
214
+ throw new Error("Invalid base models data format received.");
215
+ }
216
+ this.baseModels = result.base_models.sort();
217
+ const existingOptions = Array.from(this.searchBaseModelSelect.options);
218
+ existingOptions.slice(1).forEach(opt => opt.remove());
219
+ this.baseModels.forEach(baseModelName => {
220
+ const option = document.createElement('option');
221
+ option.value = baseModelName;
222
+ option.textContent = baseModelName;
223
+ this.searchBaseModelSelect.appendChild(option);
224
+ });
225
+ } catch (error) {
226
+ console.error("[HuggingFace] Failed to get or populate base models:", error);
227
+ this.showToast('Failed to load base models list', 'error');
228
+ }
229
+ }
230
+
231
+ switchTab(tabId) {
232
+ if (this.activeTab === tabId || !this.tabs[tabId] || !this.tabContents[tabId]) return;
233
+
234
+ this.tabs[this.activeTab]?.classList.remove('active');
235
+ this.tabContents[this.activeTab]?.classList.remove('active');
236
+
237
+ this.tabs[tabId].classList.add('active');
238
+ this.tabContents[tabId].classList.add('active');
239
+ this.tabContents[tabId].scrollTop = 0;
240
+ this.activeTab = tabId;
241
+
242
+ if (tabId === 'status') this.updateStatus();
243
+ else if (tabId === 'settings') this.applySettings();
244
+ else if(tabId === 'download') {
245
+ this.downloadConnectionsInput.value = this.settings.numConnections;
246
+ if (Object.keys(this.modelTypes).length > 0) {
247
+ this.downloadModelTypeSelect.value = this.settings.defaultModelType;
248
+ }
249
+ }
250
+ }
251
+
252
+ // --- Modal Control ---
253
+ openModal() {
254
+ this.modal?.classList.add('open');
255
+ document.body.style.setProperty('overflow', 'hidden', 'important');
256
+ this.startStatusUpdates();
257
+ if (this.activeTab === 'status') this.updateStatus();
258
+ if (!this.settings.apiKey) this.switchTab('settings');
259
+ }
260
+
261
+ closeModal() {
262
+ this.modal?.classList.remove('open');
263
+ document.body.style.removeProperty('overflow');
264
+ this.stopStatusUpdates();
265
+ }
266
+
267
+ // --- Utility Methods ---
268
+ formatBytes(bytes, decimals = 2) {
269
+ if (bytes === null || bytes === undefined || isNaN(bytes)) return 'N/A';
270
+ if (bytes === 0) return '0 Bytes';
271
+ const k = 1024;
272
+ const dm = decimals < 0 ? 0 : decimals;
273
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
274
+ const i = Math.floor(Math.log(Math.abs(bytes)) / Math.log(k));
275
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
276
+ }
277
+
278
+ formatSpeed(bytesPerSecond) {
279
+ if (!isFinite(bytesPerSecond) || bytesPerSecond <= 0) return '';
280
+ return this.formatBytes(bytesPerSecond) + '/s';
281
+ }
282
+
283
+ formatDuration(isoStart, isoEnd) {
284
+ try {
285
+ const diffSeconds = Math.round((new Date(isoEnd) - new Date(isoStart)) / 1000);
286
+ if (isNaN(diffSeconds) || diffSeconds < 0) return 'N/A';
287
+ if (diffSeconds < 60) return `${diffSeconds}s`;
288
+ const diffMinutes = Math.floor(diffSeconds / 60);
289
+ const remainingSeconds = diffSeconds % 60;
290
+ return `${diffMinutes}m ${remainingSeconds}s`;
291
+ } catch (e) {
292
+ return 'N/A';
293
+ }
294
+ }
295
+
296
+ showToast(message, type = 'info', duration = 3000) {
297
+ this.feedback?.show(message, type, duration);
298
+ }
299
+
300
+ ensureFontAwesome() {
301
+ this.feedback?.ensureFontAwesome();
302
+ }
303
+
304
+ // --- Rendering (delegated to external renderers) ---
305
+ renderDownloadList = (items, container, emptyMessage) => renderDownloadList(this, items, container, emptyMessage);
306
+ renderSearchResults = (items) => renderSearchResults(this, items);
307
+ renderDownloadPreview = (data) => renderDownloadPreview(this, data);
308
+
309
+ // --- Auto-select model type based on HuggingFace model type ---
310
+ inferFolderFromHuggingFaceType(huggingfaceType) {
311
+ if (!huggingfaceType || typeof huggingfaceType !== 'string') return null;
312
+ const t = huggingfaceType.trim().toLowerCase();
313
+ const keys = Object.keys(this.modelTypes || {});
314
+ if (keys.length === 0) return null;
315
+
316
+ const exists = (k) => keys.includes(k);
317
+ const findBy = (pred) => keys.find(pred);
318
+
319
+ // Direct matches first
320
+ if (exists(t)) return t;
321
+ if (exists(`${t}s`)) return `${t}s`;
322
+
323
+ // Common mappings from HuggingFace types to ComfyUI folders
324
+ const candidates = [];
325
+ const addIfExists = (k) => { if (exists(k)) candidates.push(k); };
326
+
327
+ switch (t) {
328
+ case 'checkpoint':
329
+ addIfExists('checkpoints');
330
+ addIfExists('models');
331
+ break;
332
+ case 'lora': case 'locon': case 'lycoris':
333
+ addIfExists('loras');
334
+ break;
335
+ case 'vae':
336
+ addIfExists('vae');
337
+ break;
338
+ case 'textualinversion': case 'embedding': case 'embeddings':
339
+ addIfExists('embeddings');
340
+ break;
341
+ case 'hypernetwork':
342
+ addIfExists('hypernetworks');
343
+ break;
344
+ case 'controlnet':
345
+ addIfExists('controlnet');
346
+ break;
347
+ case 'unet': case 'unet2':
348
+ addIfExists('unet');
349
+ break;
350
+ case 'diffusers': case 'diffusionmodels': case 'diffusion_models': case 'diffusion':
351
+ addIfExists('diffusers');
352
+ addIfExists('diffusion_models');
353
+ break;
354
+ case 'upscaler': case 'upscalers':
355
+ addIfExists('upscale_models');
356
+ addIfExists('upscalers');
357
+ break;
358
+ case 'motionmodule':
359
+ addIfExists('motion_models');
360
+ break;
361
+ case 'poses':
362
+ addIfExists('poses');
363
+ break;
364
+ case 'wildcards':
365
+ addIfExists('wildcards');
366
+ break;
367
+ case 'onnx':
368
+ addIfExists('onnx');
369
+ break;
370
+ }
371
+ if (candidates.length > 0) return candidates[0];
372
+
373
+ // Relaxed match: name contains type
374
+ const contains = findBy(k => k.toLowerCase().includes(t));
375
+ if (contains) return contains;
376
+
377
+ return null;
378
+ }
379
+
380
+ async autoSelectModelTypeFromHuggingFace(huggingfaceType) {
381
+ try {
382
+ const folder = this.inferFolderFromHuggingFaceType(huggingfaceType);
383
+ if (!folder) return;
384
+ if (this.downloadModelTypeSelect && this.downloadModelTypeSelect.value !== folder) {
385
+ this.downloadModelTypeSelect.value = folder;
386
+ await this.loadAndPopulateSubdirs(folder);
387
+ // Reset subdir to root after auto-switch
388
+ if (this.subdirSelect) this.subdirSelect.value = '';
389
+ }
390
+ } catch (e) {
391
+ console.warn('[HuggingFace] Auto-select model type failed:', e);
392
+ }
393
+ }
394
+
395
+ renderSearchPagination(metadata) {
396
+ if (!this.searchPaginationContainer) return;
397
+ if (!metadata || metadata.totalPages <= 1) {
398
+ this.searchPaginationContainer.innerHTML = '';
399
+ this.searchPagination = { ...this.searchPagination, ...metadata };
400
+ return;
401
+ }
402
+
403
+ this.searchPagination = { ...this.searchPagination, ...metadata };
404
+ const { currentPage, totalPages, totalItems } = this.searchPagination;
405
+
406
+ const createButton = (text, page, isDisabled = false, isCurrent = false) => {
407
+ const button = document.createElement('button');
408
+ button.className = `huggingface-button small huggingface-page-button ${isCurrent ? 'primary active' : ''}`;
409
+ button.dataset.page = page;
410
+ button.disabled = isDisabled;
411
+ button.innerHTML = text;
412
+ button.type = 'button';
413
+ return button;
414
+ };
415
+
416
+ const fragment = document.createDocumentFragment();
417
+ fragment.appendChild(createButton('&laquo; Prev', currentPage - 1, currentPage === 1));
418
+
419
+ let startPage = Math.max(1, currentPage - 2);
420
+ let endPage = Math.min(totalPages, currentPage + 2);
421
+
422
+ if (startPage > 1) fragment.appendChild(createButton('1', 1));
423
+ if (startPage > 2) fragment.appendChild(document.createElement('span')).textContent = '...';
424
+
425
+ for (let i = startPage; i <= endPage; i++) {
426
+ fragment.appendChild(createButton(i, i, false, i === currentPage));
427
+ }
428
+
429
+ if (endPage < totalPages - 1) fragment.appendChild(document.createElement('span')).textContent = '...';
430
+ if (endPage < totalPages) fragment.appendChild(createButton(totalPages, totalPages));
431
+
432
+ fragment.appendChild(createButton('Next &raquo;', currentPage + 1, currentPage === totalPages));
433
+
434
+ const info = document.createElement('div');
435
+ info.className = 'huggingface-pagination-info';
436
+ info.textContent = `Page ${currentPage} of ${totalPages} (${totalItems.toLocaleString()} models)`;
437
+ fragment.appendChild(info);
438
+
439
+ this.searchPaginationContainer.innerHTML = '';
440
+ this.searchPaginationContainer.appendChild(fragment);
441
+ }
442
+
443
+ // --- Event Handlers and State Management (delegated to handlers) ---
444
+ setupEventListeners = () => setupEventListeners(this);
445
+ getDefaultSettings = () => getDefaultSettings();
446
+ loadAndApplySettings = () => loadAndApplySettings(this);
447
+ loadSettingsFromCookie = () => loadSettingsFromCookie(this);
448
+ saveSettingsToCookie = () => saveSettingsToCookie(this);
449
+ applySettings = () => applySettings(this);
450
+ handleSettingsSave = () => handleSettingsSave(this);
451
+ loadGlobalRootSetting = () => loadGlobalRootSetting(this);
452
+ handleSetGlobalRoot = () => handleSetGlobalRoot(this);
453
+ handleClearGlobalRoot = () => handleClearGlobalRoot(this);
454
+ handleDownloadSubmit = () => handleDownloadSubmit(this);
455
+ handleSearchSubmit = () => handleSearchSubmit(this);
456
+ fetchAndDisplayDownloadPreview = () => fetchAndDisplayDownloadPreview(this);
457
+ debounceFetchDownloadPreview = (delay) => debounceFetchDownloadPreview(this, delay);
458
+ startStatusUpdates = () => startStatusUpdates(this);
459
+ stopStatusUpdates = () => stopStatusUpdates(this);
460
+ updateStatus = () => updateStatus(this);
461
+ handleCancelDownload = (downloadId) => handleCancelDownload(this, downloadId);
462
+ handleRetryDownload = (downloadId, button) => handleRetryDownload(this, downloadId, button);
463
+ handleOpenPath = (downloadId, button) => handleOpenPath(this, downloadId, button);
464
+ handleClearHistory = () => handleClearHistory(this);
465
+ }
custom_nodes/ComfyUI-HuggingFace/web/js/ui/feedback.js ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Centralized feedback utilities: toasts and icon CSS
2
+
3
+ export class Feedback {
4
+ constructor(toastElement) {
5
+ this.toastElement = toastElement || null;
6
+ this.toastTimeout = null;
7
+ }
8
+
9
+ ensureFontAwesome() {
10
+ if (!document.getElementById('huggingface-fontawesome-link')) {
11
+ const faLink = document.createElement('link');
12
+ faLink.id = 'huggingface-fontawesome-link';
13
+ faLink.rel = 'stylesheet';
14
+ faLink.href = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css';
15
+ faLink.integrity = 'sha512-1ycn6IcaQQ40/MKBW2W4Rhis/DbILU74C1vSrLJxCq57o941Ym01SwNsOMqvEBFlcgUa6xLiPY/NS5R+E6ztJQ==';
16
+ faLink.crossOrigin = 'anonymous';
17
+ faLink.referrerPolicy = 'no-referrer';
18
+ document.head.appendChild(faLink);
19
+ }
20
+ }
21
+
22
+ show(message, type = 'info', duration = 3000) {
23
+ if (!this.toastElement) return;
24
+ if (this.toastTimeout) {
25
+ clearTimeout(this.toastTimeout);
26
+ this.toastTimeout = null;
27
+ }
28
+ const valid = ['info', 'success', 'error', 'warning'];
29
+ const toastType = valid.includes(type) ? type : 'info';
30
+
31
+ this.toastElement.textContent = message;
32
+ this.toastElement.className = 'huggingface-toast';
33
+ this.toastElement.classList.add(toastType);
34
+ requestAnimationFrame(() => this.toastElement.classList.add('show'));
35
+ this.toastTimeout = setTimeout(() => {
36
+ this.toastElement.classList.remove('show');
37
+ this.toastTimeout = null;
38
+ }, duration);
39
+ }
40
+ }
41
+
custom_nodes/ComfyUI-HuggingFace/web/js/ui/handlers/downloadHandler.js ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { HuggingFaceDownloaderAPI } from "../../api/huggingface.js";
2
+
3
+ export function debounceFetchDownloadPreview(ui, delay = 500) {
4
+ clearTimeout(ui.modelPreviewDebounceTimeout);
5
+ ui.modelPreviewDebounceTimeout = setTimeout(() => {
6
+ fetchAndDisplayDownloadPreview(ui);
7
+ }, delay);
8
+ }
9
+
10
+ export async function fetchAndDisplayDownloadPreview(ui) {
11
+ const modelUrlOrId = ui.modelUrlInput.value.trim();
12
+ const versionId = ui.modelVersionIdInput.value.trim();
13
+
14
+ if (!modelUrlOrId) {
15
+ ui.downloadPreviewArea.innerHTML = '';
16
+ return;
17
+ }
18
+
19
+ ui.downloadPreviewArea.innerHTML = '<p><i class="fas fa-spinner fa-spin"></i> Loading model details...</p>';
20
+ ui.ensureFontAwesome();
21
+
22
+ const params = {
23
+ model_url_or_id: modelUrlOrId,
24
+ model_version_id: versionId ? parseInt(versionId, 10) : null,
25
+ api_key: ui.settings.apiKey
26
+ };
27
+
28
+ try {
29
+ const result = await HuggingFaceDownloaderAPI.getModelDetails(params);
30
+ if (result && result.success) {
31
+ ui.renderDownloadPreview(result);
32
+ // Auto-select model type save location based on HuggingFace model type
33
+ if (result.model_type) {
34
+ await ui.autoSelectModelTypeFromHuggingFace(result.model_type);
35
+ }
36
+ } else {
37
+ // Don't show error for missing details - just show neutral message
38
+ const message = result.details || result.error || 'Model details not available';
39
+ ui.downloadPreviewArea.innerHTML = `<p style="color: var(--input-text, #ccc);">${message}</p>`;
40
+ }
41
+ } catch (error) {
42
+ // Don't show scary error messages - just neutral info
43
+ const message = 'Model details not available';
44
+ console.info("Download Preview - details not available:", error);
45
+ ui.downloadPreviewArea.innerHTML = `<p style="color: var(--input-text, #ccc);">${message}</p>`;
46
+ }
47
+ }
48
+
49
+ export async function handleDownloadSubmit(ui) {
50
+ ui.downloadSubmitButton.disabled = true;
51
+ ui.downloadSubmitButton.textContent = 'Starting...';
52
+
53
+ const modelUrlOrId = ui.modelUrlInput.value.trim();
54
+ if (!modelUrlOrId) {
55
+ ui.showToast("Model URL or ID cannot be empty.", "error");
56
+ ui.downloadSubmitButton.disabled = false;
57
+ ui.downloadSubmitButton.textContent = 'Start Download';
58
+ return;
59
+ }
60
+
61
+ // Subfolder comes from dropdown; filename is base name only
62
+ const selectedSubdir = ui.subdirSelect ? ui.subdirSelect.value.trim() : '';
63
+ const userFilename = ui.customFilenameInput.value.trim();
64
+
65
+ const params = {
66
+ model_url_or_id: modelUrlOrId,
67
+ model_type: ui.downloadModelTypeSelect.value,
68
+ model_version_id: ui.modelVersionIdInput.value ? parseInt(ui.modelVersionIdInput.value, 10) : null,
69
+ custom_filename: userFilename,
70
+ subdir: selectedSubdir,
71
+ num_connections: parseInt(ui.downloadConnectionsInput.value, 10),
72
+ force_redownload: ui.forceRedownloadCheckbox.checked,
73
+ api_key: ui.settings.apiKey
74
+ };
75
+
76
+ const fileSelectEl = ui.modal.querySelector('#huggingface-file-select');
77
+ if (fileSelectEl && fileSelectEl.value) {
78
+ const fid = parseInt(fileSelectEl.value, 10);
79
+ if (!Number.isNaN(fid)) params.file_id = fid;
80
+ }
81
+
82
+ try {
83
+ const result = await HuggingFaceDownloaderAPI.downloadModel(params);
84
+
85
+ if (result.status === 'queued') {
86
+ ui.showToast(`Download queued: ${result.details?.filename || 'Model'}`, 'success');
87
+ if (ui.settings.autoOpenStatusTab) {
88
+ ui.switchTab('status');
89
+ } else {
90
+ ui.updateStatus();
91
+ }
92
+ } else if (result.status === 'exists' || result.status === 'exists_size_mismatch') {
93
+ ui.showToast(`${result.message}`, 'info', 4000);
94
+ } else {
95
+ console.warn("Unexpected success response from /huggingface/download:", result);
96
+ ui.showToast(`Unexpected status: ${result.status} - ${result.message || ''}`, 'info');
97
+ }
98
+ } catch (error) {
99
+ const message = `Download failed: ${error.details || error.message || 'Unknown error'}`;
100
+ console.error("Download Submit Error:", error);
101
+ ui.showToast(message, 'error', 6000);
102
+ } finally {
103
+ ui.downloadSubmitButton.disabled = false;
104
+ ui.downloadSubmitButton.textContent = 'Start Download';
105
+ }
106
+ }
custom_nodes/ComfyUI-HuggingFace/web/js/ui/handlers/eventListeners.js ADDED
@@ -0,0 +1,248 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { HuggingFaceDownloaderAPI } from "../../api/huggingface.js";
2
+ export function setupEventListeners(ui) {
3
+ // Modal close
4
+ ui.closeButton.addEventListener('click', () => ui.closeModal());
5
+ ui.modal.addEventListener('click', (event) => {
6
+ if (event.target === ui.modal) ui.closeModal();
7
+ });
8
+
9
+ // Tab switching
10
+ ui.tabContainer.addEventListener('click', (event) => {
11
+ if (event.target.matches('.huggingface-downloader-tab')) {
12
+ ui.switchTab(event.target.dataset.tab);
13
+ }
14
+ });
15
+
16
+ // --- FORMS ---
17
+ ui.downloadForm.addEventListener('submit', (event) => {
18
+ event.preventDefault();
19
+ ui.handleDownloadSubmit();
20
+ });
21
+
22
+ // Change of model type should refresh subdir list
23
+ ui.downloadModelTypeSelect.addEventListener('change', async () => {
24
+ await ui.loadAndPopulateSubdirs(ui.downloadModelTypeSelect.value);
25
+ });
26
+
27
+ // Create new model type folder (first-level under models/)
28
+ ui.createModelTypeButton.addEventListener('click', async () => {
29
+ const name = prompt('Enter new model type folder name (will be created under models/)');
30
+ if (!name) return;
31
+ try {
32
+ const res = await HuggingFaceDownloaderAPI.createModelType(name);
33
+ if (res && res.success) {
34
+ await ui.populateModelTypes();
35
+ ui.downloadModelTypeSelect.value = res.name;
36
+ await ui.loadAndPopulateSubdirs(res.name);
37
+ ui.showToast(`Created model type folder: ${res.name}`, 'success');
38
+ } else {
39
+ ui.showToast(res?.error || 'Failed to create model type folder', 'error');
40
+ }
41
+ } catch (e) {
42
+ ui.showToast(e.details || e.message || 'Error creating model type folder', 'error');
43
+ }
44
+ });
45
+
46
+ // Create new subfolder under current model type
47
+ ui.createSubdirButton.addEventListener('click', async () => {
48
+ const type = ui.downloadModelTypeSelect.value;
49
+ const name = prompt('Enter new subfolder name (you can include nested paths like A/B):');
50
+ if (!name) return;
51
+ try {
52
+ const res = await HuggingFaceDownloaderAPI.createModelDir(type, name);
53
+ if (res && res.success) {
54
+ await ui.loadAndPopulateSubdirs(type);
55
+ if (ui.subdirSelect) ui.subdirSelect.value = res.created || '';
56
+ ui.showToast(`Created folder: ${res.created}`, 'success');
57
+ } else {
58
+ ui.showToast(res?.error || 'Failed to create folder', 'error');
59
+ }
60
+ } catch (e) {
61
+ ui.showToast(e.details || e.message || 'Error creating folder', 'error');
62
+ }
63
+ });
64
+
65
+ ui.searchForm.addEventListener('submit', (event) => {
66
+ event.preventDefault();
67
+ if (!ui.searchQueryInput.value.trim() && ui.searchTypeSelect.value === 'any' && ui.searchBaseModelSelect.value === 'any') {
68
+ ui.showToast("Please enter a search query or select a filter.", "error");
69
+ if (ui.searchResultsContainer) ui.searchResultsContainer.innerHTML = '<p>Please enter a search query or select a filter.</p>';
70
+ if (ui.searchPaginationContainer) ui.searchPaginationContainer.innerHTML = '';
71
+ return;
72
+ }
73
+ ui.searchPagination.currentPage = 1;
74
+ ui.handleSearchSubmit();
75
+ });
76
+
77
+ ui.settingsForm.addEventListener('submit', (event) => {
78
+ event.preventDefault();
79
+ ui.handleSettingsSave();
80
+ });
81
+ if (ui.settingsSetGlobalRootButton) {
82
+ ui.settingsSetGlobalRootButton.addEventListener('click', () => {
83
+ ui.handleSetGlobalRoot();
84
+ });
85
+ }
86
+ if (ui.settingsClearGlobalRootButton) {
87
+ ui.settingsClearGlobalRootButton.addEventListener('click', () => {
88
+ ui.handleClearGlobalRoot();
89
+ });
90
+ }
91
+
92
+ // Download form inputs
93
+ ui.modelUrlInput.addEventListener('input', () => ui.debounceFetchDownloadPreview());
94
+ ui.modelUrlInput.addEventListener('paste', () => ui.debounceFetchDownloadPreview(0));
95
+ ui.modelVersionIdInput.addEventListener('blur', () => ui.fetchAndDisplayDownloadPreview());
96
+
97
+ // --- DYNAMIC CONTENT LISTENERS (Event Delegation) ---
98
+
99
+ // Status tab actions (Cancel/Retry/Open/Clear) and click-to-toggle blur on thumbs
100
+ ui.statusContent.addEventListener('click', (event) => {
101
+ const thumbContainer = event.target.closest('.huggingface-thumbnail-container');
102
+ if (thumbContainer) {
103
+ const nsfwLevel = Number(thumbContainer.dataset.nsfwLevel ?? thumbContainer.getAttribute('data-nsfw-level'));
104
+ const threshold = Number(ui.settings?.nsfwBlurMinLevel ?? 4);
105
+ const enabled = ui.settings?.hideMatureInSearch === true;
106
+ if (enabled && Number.isFinite(nsfwLevel) && nsfwLevel >= threshold) {
107
+ if (thumbContainer.classList.contains('blurred')) {
108
+ thumbContainer.classList.remove('blurred');
109
+ const overlay = thumbContainer.querySelector('.huggingface-nsfw-overlay');
110
+ if (overlay) overlay.remove();
111
+ } else {
112
+ thumbContainer.classList.add('blurred');
113
+ if (!thumbContainer.querySelector('.huggingface-nsfw-overlay')) {
114
+ const ov = document.createElement('div');
115
+ ov.className = 'huggingface-nsfw-overlay';
116
+ ov.title = 'R-rated: click to reveal';
117
+ ov.textContent = 'R';
118
+ thumbContainer.appendChild(ov);
119
+ }
120
+ }
121
+ return; // consume
122
+ }
123
+ }
124
+
125
+ const button = event.target.closest('button');
126
+ if (!button) return;
127
+
128
+ const downloadId = button.dataset.id;
129
+ if (downloadId) {
130
+ if (button.classList.contains('huggingface-cancel-button')) ui.handleCancelDownload(downloadId);
131
+ else if (button.classList.contains('huggingface-retry-button')) ui.handleRetryDownload(downloadId, button);
132
+ else if (button.classList.contains('huggingface-openpath-button')) ui.handleOpenPath(downloadId, button);
133
+ } else if (button.id === 'huggingface-clear-history-button') {
134
+ ui.confirmClearModal.style.display = 'flex';
135
+ }
136
+ });
137
+
138
+ // Download preview click-to-toggle blur
139
+ ui.downloadPreviewArea.addEventListener('click', (event) => {
140
+ const thumbContainer = event.target.closest('.huggingface-thumbnail-container');
141
+ if (thumbContainer) {
142
+ const nsfwLevel = Number(thumbContainer.dataset.nsfwLevel ?? thumbContainer.getAttribute('data-nsfw-level'));
143
+ const threshold = Number(ui.settings?.nsfwBlurMinLevel ?? 4);
144
+ const enabled = ui.settings?.hideMatureInSearch === true;
145
+ if (enabled && Number.isFinite(nsfwLevel) && nsfwLevel >= threshold) {
146
+ if (thumbContainer.classList.contains('blurred')) {
147
+ thumbContainer.classList.remove('blurred');
148
+ const overlay = thumbContainer.querySelector('.huggingface-nsfw-overlay');
149
+ if (overlay) overlay.remove();
150
+ } else {
151
+ thumbContainer.classList.add('blurred');
152
+ if (!thumbContainer.querySelector('.huggingface-nsfw-overlay')) {
153
+ const ov = document.createElement('div');
154
+ ov.className = 'huggingface-nsfw-overlay';
155
+ ov.title = 'R-rated: click to reveal';
156
+ ov.textContent = 'R';
157
+ thumbContainer.appendChild(ov);
158
+ }
159
+ }
160
+ }
161
+ }
162
+ });
163
+
164
+ // Search results actions, including click-to-toggle blur
165
+ ui.searchResultsContainer.addEventListener('click', (event) => {
166
+ const thumbContainer = event.target.closest('.huggingface-thumbnail-container');
167
+ if (thumbContainer) {
168
+ const nsfwLevel = Number(thumbContainer.dataset.nsfwLevel ?? thumbContainer.getAttribute('data-nsfw-level'));
169
+ const threshold = Number(ui.settings?.nsfwBlurMinLevel ?? 4);
170
+ const enabled = ui.settings?.hideMatureInSearch === true;
171
+ if (enabled && Number.isFinite(nsfwLevel) && nsfwLevel >= threshold) {
172
+ if (thumbContainer.classList.contains('blurred')) {
173
+ thumbContainer.classList.remove('blurred');
174
+ const overlay = thumbContainer.querySelector('.huggingface-nsfw-overlay');
175
+ if (overlay) overlay.remove();
176
+ } else {
177
+ thumbContainer.classList.add('blurred');
178
+ if (!thumbContainer.querySelector('.huggingface-nsfw-overlay')) {
179
+ const ov = document.createElement('div');
180
+ ov.className = 'huggingface-nsfw-overlay';
181
+ ov.title = 'R-rated: click to reveal';
182
+ ov.textContent = 'R';
183
+ thumbContainer.appendChild(ov);
184
+ }
185
+ }
186
+ return; // Don't trigger other actions on this click
187
+ }
188
+ }
189
+
190
+ const downloadButton = event.target.closest('.huggingface-search-download-button');
191
+ if (downloadButton) {
192
+ event.preventDefault();
193
+ const { modelId, versionId, modelType, creator, modelName } = downloadButton.dataset;
194
+ if (!modelId) {
195
+ ui.showToast("Error: Missing model ID for download.", "error");
196
+ return;
197
+ }
198
+ const modelTypeInternalKey = Object.keys(ui.modelTypes).find(key => ui.modelTypes[key]?.toLowerCase() === modelType?.toLowerCase()) || ui.settings.defaultModelType;
199
+
200
+ ui.modelUrlInput.value = modelId;
201
+ ui.modelVersionIdInput.value = versionId;
202
+ ui.customFilenameInput.value = modelName ? `${modelName.replace(/[^a-zA-Z0-9_-]/g, '_')}` : '';
203
+ ui.forceRedownloadCheckbox.checked = false;
204
+ ui.downloadModelTypeSelect.value = modelTypeInternalKey;
205
+
206
+ ui.switchTab('download');
207
+ ui.showToast(`Filled download form for "${modelName || modelId}" by ${creator || 'Unknown'}.`, 'info', 4000);
208
+ ui.fetchAndDisplayDownloadPreview();
209
+ return;
210
+ }
211
+
212
+ const viewAllButton = event.target.closest('.show-all-versions-button');
213
+ if (viewAllButton) {
214
+ const modelId = viewAllButton.dataset.modelId;
215
+ const versionsContainer = ui.searchResultsContainer.querySelector(`#all-versions-${modelId}`);
216
+ if (versionsContainer) {
217
+ const currentlyVisible = versionsContainer.style.display !== 'none';
218
+ versionsContainer.style.display = currentlyVisible ? 'none' : 'flex';
219
+ viewAllButton.innerHTML = currentlyVisible
220
+ ? `All versions (${viewAllButton.dataset.totalVersions}) <i class="fas fa-chevron-down"></i>`
221
+ : `Show less <i class="fas fa-chevron-up"></i>`;
222
+ }
223
+ }
224
+ });
225
+
226
+ // Pagination
227
+ ui.searchPaginationContainer.addEventListener('click', (event) => {
228
+ const button = event.target.closest('.huggingface-page-button');
229
+ if (button && !button.disabled) {
230
+ const page = parseInt(button.dataset.page, 10);
231
+ if (page && page !== ui.searchPagination.currentPage) {
232
+ ui.searchPagination.currentPage = page;
233
+ ui.handleSearchSubmit();
234
+ }
235
+ }
236
+ });
237
+
238
+ // Confirmation Modal
239
+ ui.confirmClearYesButton.addEventListener('click', () => ui.handleClearHistory());
240
+ ui.confirmClearNoButton.addEventListener('click', () => {
241
+ ui.confirmClearModal.style.display = 'none';
242
+ });
243
+ ui.confirmClearModal.addEventListener('click', (event) => {
244
+ if (event.target === ui.confirmClearModal) {
245
+ ui.confirmClearModal.style.display = 'none';
246
+ }
247
+ });
248
+ }
custom_nodes/ComfyUI-HuggingFace/web/js/ui/handlers/searchHandler.js ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { HuggingFaceDownloaderAPI } from "../../api/huggingface.js";
2
+
3
+ export async function handleSearchSubmit(ui) {
4
+ ui.searchSubmitButton.disabled = true;
5
+ ui.searchSubmitButton.textContent = 'Searching...';
6
+ ui.searchResultsContainer.innerHTML = '<p><i class="fas fa-spinner fa-spin"></i> Searching...</p>';
7
+ ui.searchPaginationContainer.innerHTML = '';
8
+ ui.ensureFontAwesome();
9
+
10
+ const params = {
11
+ query: ui.searchQueryInput.value.trim(),
12
+ model_types: ui.searchTypeSelect.value === 'any' ? [] : [ui.searchTypeSelect.value],
13
+ base_models: ui.searchBaseModelSelect.value === 'any' ? [] : [ui.searchBaseModelSelect.value],
14
+ sort: ui.searchSortSelect.value,
15
+ limit: ui.searchPagination.limit,
16
+ page: ui.searchPagination.currentPage,
17
+ api_key: ui.settings.apiKey,
18
+ };
19
+
20
+ try {
21
+ const response = await HuggingFaceDownloaderAPI.searchModels(params);
22
+ if (!response || !response.metadata || !Array.isArray(response.items)) {
23
+ console.error("Invalid search response structure:", response);
24
+ throw new Error("Received invalid data from search API.");
25
+ }
26
+
27
+ ui.renderSearchResults(response.items);
28
+ ui.renderSearchPagination(response.metadata);
29
+
30
+ } catch (error) {
31
+ const message = `Search failed: ${error.details || error.message || 'Unknown error'}`;
32
+ console.error("Search Submit Error:", error);
33
+ ui.searchResultsContainer.innerHTML = `<p style="color: var(--error-text, #ff6b6b);">${message}</p>`;
34
+ ui.showToast(message, 'error');
35
+ } finally {
36
+ ui.searchSubmitButton.disabled = false;
37
+ ui.searchSubmitButton.textContent = 'Search';
38
+ }
39
+ }
custom_nodes/ComfyUI-HuggingFace/web/js/ui/handlers/settingsHandler.js ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { setCookie, getCookie } from "../../utils/cookies.js";
2
+ import { HuggingFaceDownloaderAPI } from "../../api/huggingface.js";
3
+
4
+ const SETTINGS_COOKIE_NAME = 'huggingfaceDownloaderSettings';
5
+
6
+ export function getDefaultSettings() {
7
+ return {
8
+ apiKey: '',
9
+ numConnections: 1,
10
+ defaultModelType: 'checkpoints',
11
+ autoOpenStatusTab: true,
12
+ searchResultLimit: 20,
13
+ hideMatureInSearch: true,
14
+ nsfwBlurMinLevel: 4, // Blur thumbnails with nsfwLevel >= this value
15
+ };
16
+ }
17
+
18
+ export function loadAndApplySettings(ui) {
19
+ ui.settings = ui.loadSettingsFromCookie();
20
+ ui.applySettings();
21
+ }
22
+
23
+ export function loadSettingsFromCookie(ui) {
24
+ const defaults = ui.getDefaultSettings();
25
+ const cookieValue = getCookie(SETTINGS_COOKIE_NAME);
26
+
27
+ if (cookieValue) {
28
+ try {
29
+ const loadedSettings = JSON.parse(cookieValue);
30
+ return { ...defaults, ...loadedSettings };
31
+ } catch (e) {
32
+ console.error("Failed to parse settings cookie:", e);
33
+ return defaults;
34
+ }
35
+ }
36
+ return defaults;
37
+ }
38
+
39
+ export function saveSettingsToCookie(ui) {
40
+ try {
41
+ const settingsString = JSON.stringify(ui.settings);
42
+ setCookie(SETTINGS_COOKIE_NAME, settingsString, 365);
43
+ ui.showToast('Settings saved successfully!', 'success');
44
+ } catch (e) {
45
+ console.error("Failed to save settings to cookie:", e);
46
+ ui.showToast('Error saving settings', 'error');
47
+ }
48
+ }
49
+
50
+ export function applySettings(ui) {
51
+ if (ui.settingsApiKeyInput) {
52
+ ui.settingsApiKeyInput.value = ui.settings.apiKey || '';
53
+ }
54
+ if (ui.settingsConnectionsInput) {
55
+ ui.settingsConnectionsInput.value = Math.max(1, Math.min(16, ui.settings.numConnections || 1));
56
+ }
57
+ if (ui.settingsDefaultTypeSelect) {
58
+ const desired = ui.settings.defaultModelType || 'checkpoints';
59
+ ui.settingsDefaultTypeSelect.value = desired;
60
+ if (!ui.settingsDefaultTypeSelect.querySelector(`option[value="${ui.settingsDefaultTypeSelect.value}"]`)) {
61
+ const first = ui.settingsDefaultTypeSelect.querySelector('option');
62
+ if (first) ui.settingsDefaultTypeSelect.value = first.value;
63
+ }
64
+ }
65
+ if (ui.settingsAutoOpenCheckbox) {
66
+ ui.settingsAutoOpenCheckbox.checked = ui.settings.autoOpenStatusTab === true;
67
+ }
68
+ if (ui.settingsHideMatureCheckbox) {
69
+ ui.settingsHideMatureCheckbox.checked = ui.settings.hideMatureInSearch === true;
70
+ }
71
+ if (ui.settingsNsfwThresholdInput) {
72
+ const val = Number(ui.settings.nsfwBlurMinLevel);
73
+ ui.settingsNsfwThresholdInput.value = Number.isFinite(val) ? val : 4;
74
+ }
75
+ if (ui.downloadConnectionsInput) {
76
+ ui.downloadConnectionsInput.value = Math.max(1, Math.min(16, ui.settings.numConnections || 1));
77
+ }
78
+ if (ui.downloadModelTypeSelect && Object.keys(ui.modelTypes).length > 0) {
79
+ const desired = ui.settings.defaultModelType || 'checkpoints';
80
+ ui.downloadModelTypeSelect.value = desired;
81
+ if (!ui.downloadModelTypeSelect.querySelector(`option[value="${ui.downloadModelTypeSelect.value}"]`)) {
82
+ const first = ui.downloadModelTypeSelect.querySelector('option');
83
+ if (first) ui.downloadModelTypeSelect.value = first.value;
84
+ }
85
+ }
86
+ ui.searchPagination.limit = ui.settings.searchResultLimit || 20;
87
+ }
88
+
89
+ export async function loadGlobalRootSetting(ui) {
90
+ if (!ui.settingsGlobalRootInput) return;
91
+ try {
92
+ const result = await HuggingFaceDownloaderAPI.getGlobalRoot();
93
+ const globalRoot = (result && typeof result.global_root === 'string') ? result.global_root : '';
94
+ ui.settingsGlobalRootInput.value = globalRoot;
95
+ } catch (e) {
96
+ console.warn("[HuggingFace] Failed to load global root setting:", e);
97
+ }
98
+ }
99
+
100
+ export async function handleSetGlobalRoot(ui) {
101
+ if (!ui.settingsGlobalRootInput) return;
102
+ const path = ui.settingsGlobalRootInput.value.trim();
103
+ if (!path) {
104
+ ui.showToast("Please enter a global root path first.", "error");
105
+ return;
106
+ }
107
+ try {
108
+ const result = await HuggingFaceDownloaderAPI.setGlobalRoot(path);
109
+ const saved = (result && typeof result.global_root === 'string') ? result.global_root : path;
110
+ ui.settingsGlobalRootInput.value = saved;
111
+ ui.showToast("Global root updated.", "success");
112
+ if (ui.downloadModelTypeSelect) {
113
+ await ui.loadAndPopulateSubdirs(ui.downloadModelTypeSelect.value);
114
+ }
115
+ } catch (e) {
116
+ ui.showToast(e.details || e.message || "Failed to set global root.", "error", 6000);
117
+ }
118
+ }
119
+
120
+ export async function handleClearGlobalRoot(ui) {
121
+ if (!ui.settingsGlobalRootInput) return;
122
+ try {
123
+ await HuggingFaceDownloaderAPI.clearGlobalRoot();
124
+ ui.settingsGlobalRootInput.value = "";
125
+ ui.showToast("Global root cleared. Using default ComfyUI paths.", "success");
126
+ if (ui.downloadModelTypeSelect) {
127
+ await ui.loadAndPopulateSubdirs(ui.downloadModelTypeSelect.value);
128
+ }
129
+ } catch (e) {
130
+ ui.showToast(e.details || e.message || "Failed to clear global root.", "error", 6000);
131
+ }
132
+ }
133
+
134
+ export function handleSettingsSave(ui) {
135
+ const apiKey = ui.settingsApiKeyInput.value.trim();
136
+ const numConnections = parseInt(ui.settingsConnectionsInput.value, 10);
137
+ const defaultModelType = ui.settingsDefaultTypeSelect.value;
138
+ const autoOpenStatusTab = ui.settingsAutoOpenCheckbox.checked;
139
+ const hideMatureInSearch = ui.settingsHideMatureCheckbox.checked;
140
+ const nsfwBlurMinLevel = Number(ui.settingsNsfwThresholdInput.value);
141
+
142
+ if (isNaN(numConnections) || numConnections < 1 || numConnections > 16) {
143
+ ui.showToast("Invalid Default Connections (must be 1-16).", "error");
144
+ return;
145
+ }
146
+ if (!ui.settingsDefaultTypeSelect.querySelector(`option[value="${defaultModelType}"]`)) {
147
+ ui.showToast("Invalid Default Model Type selected.", "error");
148
+ return;
149
+ }
150
+
151
+ ui.settings.apiKey = apiKey;
152
+ ui.settings.numConnections = numConnections;
153
+ ui.settings.defaultModelType = defaultModelType;
154
+ ui.settings.autoOpenStatusTab = autoOpenStatusTab;
155
+ ui.settings.hideMatureInSearch = hideMatureInSearch;
156
+ ui.settings.nsfwBlurMinLevel = (Number.isFinite(nsfwBlurMinLevel) && nsfwBlurMinLevel >= 0) ? Math.min(128, Math.round(nsfwBlurMinLevel)) : 4;
157
+
158
+ ui.saveSettingsToCookie();
159
+ ui.applySettings();
160
+ }
custom_nodes/ComfyUI-HuggingFace/web/js/ui/handlers/statusHandler.js ADDED
@@ -0,0 +1,154 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { HuggingFaceDownloaderAPI } from "../../api/huggingface.js";
2
+
3
+ export function startStatusUpdates(ui) {
4
+ if (!ui.statusInterval) {
5
+ console.log("[HuggingFace] Starting status updates (every 3s)...");
6
+ ui.updateStatus();
7
+ ui.statusInterval = setInterval(() => ui.updateStatus(), 3000);
8
+ }
9
+ }
10
+
11
+ export function stopStatusUpdates(ui) {
12
+ if (ui.statusInterval) {
13
+ clearInterval(ui.statusInterval);
14
+ ui.statusInterval = null;
15
+ console.log("[HuggingFace] Stopped status updates.");
16
+ }
17
+ }
18
+
19
+ export async function updateStatus(ui) {
20
+ if (!ui.modal || !ui.modal.classList.contains('open')) return;
21
+
22
+ try {
23
+ const newStatusData = await HuggingFaceDownloaderAPI.getStatus();
24
+ if (!newStatusData || !Array.isArray(newStatusData.active) || !Array.isArray(newStatusData.queue) || !Array.isArray(newStatusData.history)) {
25
+ throw new Error("Invalid status data structure received from server.");
26
+ }
27
+
28
+ const oldStateString = JSON.stringify(ui.statusData);
29
+ const newStateString = JSON.stringify(newStatusData);
30
+
31
+ // Cache new state if it differs
32
+ if (oldStateString !== newStateString) {
33
+ ui.statusData = newStatusData;
34
+ }
35
+
36
+ // Always keep counters in sync
37
+ const activeCount = ui.statusData.active.length + ui.statusData.queue.length;
38
+ ui.activeCountSpan.textContent = activeCount;
39
+ ui.statusIndicator.style.display = activeCount > 0 ? 'inline' : 'none';
40
+
41
+ // Always render when Status tab is active, even if data hasn't changed
42
+ if (ui.activeTab === 'status') {
43
+ ui.renderDownloadList(ui.statusData.active, ui.activeListContainer, 'No active downloads.');
44
+ ui.renderDownloadList(ui.statusData.queue, ui.queuedListContainer, 'Download queue is empty.');
45
+ ui.renderDownloadList(ui.statusData.history, ui.historyListContainer, 'No download history yet.');
46
+ }
47
+ } catch (error) {
48
+ console.error("[HuggingFace] Failed to update status:", error);
49
+ if (ui.activeTab === 'status') {
50
+ const errorHtml = `<p style="color: var(--error-text, #ff6b6b);">${error.details || error.message}</p>`;
51
+ if (ui.activeListContainer) ui.activeListContainer.innerHTML = errorHtml;
52
+ if (ui.queuedListContainer) ui.queuedListContainer.innerHTML = '';
53
+ if (ui.historyListContainer) ui.historyListContainer.innerHTML = '';
54
+ }
55
+ }
56
+ }
57
+
58
+ export async function handleCancelDownload(ui, downloadId) {
59
+ const button = ui.modal.querySelector(`.huggingface-cancel-button[data-id="${downloadId}"]`);
60
+ if (button) {
61
+ button.disabled = true;
62
+ button.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
63
+ button.title = "Cancelling...";
64
+ }
65
+ try {
66
+ const result = await HuggingFaceDownloaderAPI.cancelDownload(downloadId);
67
+ ui.showToast(result.message || `Cancellation requested for ${downloadId}`, 'info');
68
+ ui.updateStatus();
69
+ } catch (error) {
70
+ const message = `Cancel failed: ${error.details || error.message}`;
71
+ console.error("Cancel Download Error:", error);
72
+ ui.showToast(message, 'error');
73
+ if (button) {
74
+ button.disabled = false;
75
+ button.innerHTML = '<i class="fas fa-times"></i>';
76
+ button.title = "Cancel Download";
77
+ }
78
+ }
79
+ }
80
+
81
+ export async function handleRetryDownload(ui, downloadId, button) {
82
+ button.disabled = true;
83
+ button.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
84
+ button.title = "Retrying...";
85
+ try {
86
+ const result = await HuggingFaceDownloaderAPI.retryDownload(downloadId);
87
+ if (result.success) {
88
+ ui.showToast(result.message || `Retry queued successfully!`, 'success');
89
+ if (ui.settings.autoOpenStatusTab) ui.switchTab('status');
90
+ else ui.updateStatus();
91
+ } else {
92
+ ui.showToast(`Retry failed: ${result.details || result.error}`, 'error', 5000);
93
+ button.disabled = false;
94
+ button.innerHTML = '<i class="fas fa-redo"></i>';
95
+ button.title = "Retry Download";
96
+ }
97
+ } catch (error) {
98
+ const message = `Retry failed: ${error.details || error.message}`;
99
+ console.error("Retry Download UI Error:", error);
100
+ ui.showToast(message, 'error', 5000);
101
+ button.disabled = false;
102
+ button.innerHTML = '<i class="fas fa-redo"></i>';
103
+ button.title = "Retry Download";
104
+ }
105
+ }
106
+
107
+ export async function handleOpenPath(ui, downloadId, button) {
108
+ const originalIcon = button.innerHTML;
109
+ button.disabled = true;
110
+ button.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
111
+ button.title = "Opening...";
112
+ try {
113
+ const result = await HuggingFaceDownloaderAPI.openPath(downloadId);
114
+ if (result.success) {
115
+ ui.showToast(result.message || `Opened path successfully!`, 'success');
116
+ } else {
117
+ ui.showToast(`Open path failed: ${result.details || result.error}`, 'error', 5000);
118
+ }
119
+ } catch (error) {
120
+ const message = `Open path failed: ${error.details || error.message}`;
121
+ console.error("Open Path UI Error:", error);
122
+ ui.showToast(message, 'error', 5000);
123
+ } finally {
124
+ button.disabled = false;
125
+ button.innerHTML = originalIcon;
126
+ button.title = "Open Containing Folder";
127
+ }
128
+ }
129
+
130
+ export async function handleClearHistory(ui) {
131
+ ui.confirmClearYesButton.disabled = true;
132
+ ui.confirmClearNoButton.disabled = true;
133
+ ui.confirmClearYesButton.textContent = 'Clearing...';
134
+
135
+ try {
136
+ const result = await HuggingFaceDownloaderAPI.clearHistory();
137
+ if (result.success) {
138
+ ui.showToast(result.message || 'History cleared successfully!', 'success');
139
+ ui.statusData.history = [];
140
+ ui.renderDownloadList(ui.statusData.history, ui.historyListContainer, 'No download history yet.');
141
+ ui.confirmClearModal.style.display = 'none';
142
+ } else {
143
+ ui.showToast(`Clear history failed: ${result.details || result.error}`, 'error', 5000);
144
+ }
145
+ } catch (error) {
146
+ const message = `Clear history failed: ${error.details || error.message}`;
147
+ console.error("Clear History UI Error:", error);
148
+ ui.showToast(message, 'error', 5000);
149
+ } finally {
150
+ ui.confirmClearYesButton.disabled = false;
151
+ ui.confirmClearNoButton.disabled = false;
152
+ ui.confirmClearYesButton.textContent = 'Confirm Clear';
153
+ }
154
+ }
custom_nodes/ComfyUI-HuggingFace/web/js/ui/previewRenderer.js ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Renders the download preview panel
2
+
3
+ const PLACEHOLDER_IMAGE_URL = `/extensions/ComfyUI-HuggingFace/images/placeholder.jpg`;
4
+
5
+ export function renderDownloadPreview(ui, data) {
6
+ if (!ui.downloadPreviewArea) return;
7
+ ui.ensureFontAwesome();
8
+
9
+ const modelId = data.model_id;
10
+ const modelName = data.model_name || 'Untitled Model';
11
+ const creator = data.creator_username || 'Unknown Creator';
12
+ const modelType = data.model_type || 'N/A';
13
+ const versionName = data.version_name || 'N/A';
14
+ const baseModel = data.base_model || 'N/A';
15
+ const stats = data.stats || {};
16
+ const descriptionHtml = data.description_html || '<p><em>No description.</em></p>';
17
+ const version_description_html = data.version_description_html || '<p><em>No description.</em></p>';
18
+ const fileInfo = data.file_info || {};
19
+ const files = Array.isArray(data.files) ? data.files : [];
20
+ const thumbnail = data.thumbnail_url || PLACEHOLDER_IMAGE_URL;
21
+ const nsfwLevel = Number(data.nsfw_level ?? 0);
22
+ const blurMinLevel = Number(ui.settings?.nsfwBlurMinLevel ?? 4);
23
+ const shouldBlur = ui.settings?.hideMatureInSearch === true && nsfwLevel >= blurMinLevel;
24
+ const huggingfaceLink = `https://huggingface.com/models/${modelId}${data.version_id ? '?modelVersionId=' + data.version_id : ''}`;
25
+
26
+ const onErrorScript = `this.onerror=null; this.src='${PLACEHOLDER_IMAGE_URL}'; this.style.backgroundColor='#444';`;
27
+
28
+ const overlayHtml = shouldBlur ? `<div class="huggingface-nsfw-overlay" title="R-rated: click to reveal">R</div>` : '';
29
+ const containerClasses = `huggingface-thumbnail-container${shouldBlur ? ' blurred' : ''}`;
30
+
31
+ const previewHtml = `
32
+ <div class="huggingface-search-item" style="background-color: var(--comfy-input-bg);">
33
+ <div class="${containerClasses}" data-nsfw-level="${Number.isFinite(nsfwLevel) ? nsfwLevel : ''}">
34
+ <img src="${thumbnail}" alt="${modelName} thumbnail" class="huggingface-search-thumbnail" loading="lazy" onerror="${onErrorScript}">
35
+ ${overlayHtml}
36
+ <div class="huggingface-type-badge">${modelType}</div>
37
+ </div>
38
+ <div class="huggingface-search-info">
39
+ <h4>${modelName} <span style="font-weight: normal; font-size: 0.9em;">by ${creator}</span></h4>
40
+ <p style="font-weight: bold;">Version: ${versionName} <span class="base-model-badge" style="margin-left: 5px;">${baseModel}</span></p>
41
+ <div class="huggingface-search-stats" title="Stats: Downloads / Rating (Count) / Likes">
42
+ <span title="Downloads"><i class="fas fa-download"></i> ${stats.downloads?.toLocaleString() || 0}</span>
43
+ <span title="Likes"><i class="fas fa-thumbs-up"></i> ${stats.likes?.toLocaleString(0) || 0}</span>
44
+ <span title="Dislikes"><i class="fas fa-thumbs-down"></i> ${stats.dislikes?.toLocaleString() || 0}</span>
45
+ <span title="Buzz"><i class="fas fa-bolt"></i> ${stats.buzz?.toLocaleString() || 0}</span>
46
+ </div>
47
+ <p style="font-weight: bold; margin-top: 10px;">Primary File:</p>
48
+ <p style="font-size: 0.9em; color: #ccc;">
49
+ Name: ${fileInfo.name || 'N/A'}<br>
50
+ Size: ${ui.formatBytes(fileInfo.size_kb * 1024) || 'N/A'} <br>
51
+ Format: ${fileInfo.format || 'N/A'}<br>
52
+ Precision: ${fileInfo.precision || 'N/A'}<br>
53
+ Model Size: ${fileInfo.model_size || 'N/A'}
54
+ </p>
55
+ ${files.length > 0 ? `
56
+ <div class=\"huggingface-form-group\" style=\"margin-top: 10px;\">
57
+ <label for=\"huggingface-file-select\">Choose File (optional)</label>
58
+ <select id=\"huggingface-file-select\" class=\"huggingface-select\">
59
+ <option value=\"\">Auto (primary/best)</option>
60
+ ${files.map(f => {
61
+ const id = f.id ?? '';
62
+ const name = (f.name || '').replace(/</g,'&lt;');
63
+ const fmt = f.format || 'N/A';
64
+ const prec = (f.precision || '').toUpperCase();
65
+ const msize = f.model_size || '';
66
+ const size = (typeof f.size_kb === 'number') ? ui.formatBytes(f.size_kb * 1024) : 'N/A';
67
+ const disabled = f.downloadable ? '' : 'disabled';
68
+ const title = f.downloadable ? '' : ' (not downloadable)';
69
+ const extras = [prec, msize].filter(Boolean).join(' • ');
70
+ return `<option value=\"${id}\" ${disabled}>#${id} • ${name} • ${fmt}${extras ? ' • ' + extras : ''} • ${size}${title}</option>`;
71
+ }).join('')}
72
+ </select>
73
+ <p style=\"font-size: 0.9em; color: #aaa; margin-top: 6px;\">Pick other variants when available.</p>
74
+ </div>
75
+ ` : ''}
76
+ <a href="${huggingfaceLink}" target="_blank" rel="noopener noreferrer" class="huggingface-button small" title="Open on HuggingFace website" style="margin-top: 5px; display: inline-block;">
77
+ View on HuggingFace <i class="fas fa-external-link-alt"></i>
78
+ </a>
79
+ </div>
80
+ </div>
81
+ <div style="margin-top: 15px;">
82
+ <h5 style="margin-bottom: 5px;">Model Description:</h5>
83
+ <div class="model-description-content" style="max-height: 200px; overflow-y: auto; background-color: var(--comfy-input-bg); padding: 10px; border-radius: 4px; font-size: 0.9em; border: 1px solid var(--border-color, #555);">
84
+ ${descriptionHtml}
85
+ </div>
86
+ </div>
87
+ <div style="margin-top: 15px;">
88
+ <h5 style="margin-bottom: 5px;">Version Description:</h5>
89
+ <div class="model-description-content" style="max-height: 200px; overflow-y: auto; background-color: var(--comfy-input-bg); padding: 10px; border-radius: 4px; font-size: 0.9em; border: 1px solid var(--border-color, #555);">
90
+ ${version_description_html}
91
+ </div>
92
+ </div>
93
+ `;
94
+
95
+ ui.downloadPreviewArea.innerHTML = previewHtml;
96
+ }
custom_nodes/ComfyUI-HuggingFace/web/js/ui/searchRenderer.js ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Rendering of search results list
2
+ // Usage: renderSearchResults(uiInstance, itemsArray)
3
+
4
+ const PLACEHOLDER_IMAGE_URL = `/extensions/ComfyUI-HuggingFace/images/placeholder.jpg`;
5
+
6
+ export function renderSearchResults(ui, items) {
7
+ ui.feedback?.ensureFontAwesome();
8
+
9
+ if (!items || items.length === 0) {
10
+ const queryUsed = ui.searchQueryInput && ui.searchQueryInput.value.trim();
11
+ const typeFilterUsed = ui.searchTypeSelect && ui.searchTypeSelect.value !== 'any';
12
+ const baseModelFilterUsed = ui.searchBaseModelSelect && ui.searchBaseModelSelect.value !== 'any';
13
+ const message = (queryUsed || typeFilterUsed || baseModelFilterUsed)
14
+ ? 'No models found matching your criteria.'
15
+ : 'Enter a query or select filters and click Search.';
16
+ ui.searchResultsContainer.innerHTML = `<p>${message}</p>`;
17
+ return;
18
+ }
19
+
20
+ const placeholder = PLACEHOLDER_IMAGE_URL;
21
+ const onErrorScript = `this.onerror=null; this.src='${placeholder}'; this.style.backgroundColor='#444';`;
22
+ const fragment = document.createDocumentFragment();
23
+
24
+ items.forEach(hit => {
25
+ const modelId = hit.id;
26
+ if (!modelId) return;
27
+
28
+ // Extract base repository ID from full path (remove /tree/main, /blob/main, etc.)
29
+ const baseRepoId = modelId.split('/tree/')[0].split('/blob/')[0].split('/raw/')[0];
30
+
31
+ const creator = hit.user?.username || 'Unknown Creator';
32
+ const modelName = hit.name || 'Untitled Model';
33
+ const modelTypeApi = hit.type || 'other';
34
+ console.log('Model type for badge:', modelTypeApi);
35
+ const stats = hit.metrics || {};
36
+ const tags = hit.tags?.map(t => t.name) || [];
37
+
38
+ const thumbnailUrl = hit.thumbnailUrl || placeholder;
39
+ const firstImage = Array.isArray(hit.images) && hit.images.length > 0 ? hit.images[0] : null;
40
+ const thumbnailType = firstImage?.type;
41
+ const nsfwLevel = Number(firstImage?.nsfwLevel ?? hit.nsfwLevel ?? 0);
42
+ const blurMinLevel = Number(ui.settings?.nsfwBlurMinLevel ?? 4);
43
+ const shouldBlur = ui.settings?.hideMatureInSearch === true && nsfwLevel >= blurMinLevel;
44
+
45
+ const allVersions = hit.versions || [];
46
+ const primaryVersion = hit.version || (allVersions.length > 0 ? allVersions[0] : {});
47
+ const primaryVersionId = primaryVersion.id;
48
+ const primaryBaseModel = primaryVersion.baseModel || 'N/A';
49
+
50
+ const uniqueBaseModels = allVersions.length > 0
51
+ ? [...new Set(allVersions.map(v => v.baseModel).filter(Boolean))]
52
+ : (primaryBaseModel !== 'N/A' ? [primaryBaseModel] : []);
53
+ const baseModelsDisplay = uniqueBaseModels.length > 0 ? uniqueBaseModels.join(', ') : 'N/A';
54
+
55
+ const publishedAt = hit.publishedAt;
56
+ let lastUpdatedFormatted = 'N/A';
57
+ if (publishedAt) {
58
+ try {
59
+ const date = new Date(publishedAt);
60
+ lastUpdatedFormatted = date.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
61
+ } catch (_) {}
62
+ }
63
+
64
+ const listItem = document.createElement('div');
65
+ listItem.className = 'huggingface-search-item';
66
+ listItem.dataset.modelId = modelId;
67
+
68
+ const MAX_VISIBLE_VERSIONS = 3;
69
+ let visibleVersions = [];
70
+ if (primaryVersionId) {
71
+ visibleVersions.push({ id: primaryVersionId, name: primaryVersion.name || 'Primary Version', baseModel: primaryBaseModel });
72
+ }
73
+ allVersions.forEach(v => {
74
+ if (v.id !== primaryVersionId && visibleVersions.length < MAX_VISIBLE_VERSIONS) visibleVersions.push(v);
75
+ });
76
+
77
+ let versionButtonsHtml = visibleVersions.map(version => {
78
+ const versionId = version.id;
79
+ const versionName = version.name || 'Unknown Version';
80
+ const baseModel = version.baseModel || 'N/A';
81
+ return `
82
+ <button class="huggingface-button primary small huggingface-search-download-button"
83
+ data-model-id="${baseRepoId}"
84
+ data-version-id="${versionId || ''}"
85
+ data-model-type="${modelTypeApi || ''}"
86
+ data-creator="${creator}"
87
+ data-model-name="${modelName}"
88
+ title="${!versionId ? 'Download latest version' : 'Pre-fill Download Tab'}" >
89
+ <span class="base-model-badge">${baseModel}</span> ${versionName} <i class="fas fa-download"></i>
90
+ </button>
91
+ `;
92
+ }).join('');
93
+
94
+ const hasMoreVersions = allVersions.length > visibleVersions.length;
95
+ const totalVersionCount = allVersions.length;
96
+ const moreButtonHtml = hasMoreVersions ? `
97
+ <button class="huggingface-button secondary small show-all-versions-button"
98
+ data-model-id="${modelId}"
99
+ data-total-versions="${totalVersionCount}"
100
+ title="Show all ${totalVersionCount} versions">
101
+ All versions (${totalVersionCount}) <i class="fas fa-chevron-down"></i>
102
+ </button>
103
+ ` : '';
104
+
105
+ let allVersionsHtml = '';
106
+ if (hasMoreVersions) {
107
+ const hiddenVersions = allVersions.filter(v => !visibleVersions.some(vis => vis.id === v.id));
108
+ allVersionsHtml = `
109
+ <div class="all-versions-container" id="all-versions-${modelId}" style="display: none;">
110
+ ${hiddenVersions.map(version => {
111
+ const versionId = version.id;
112
+ const versionName = version.name || 'Unknown Version';
113
+ const baseModel = version.baseModel || 'N/A';
114
+ return `
115
+ <button class="huggingface-button primary small huggingface-search-download-button"
116
+ data-model-id="${baseRepoId}"
117
+ data-version-id="${versionId || ''}"
118
+ data-model-type="${modelTypeApi || ''}"
119
+ data-creator="${creator}"
120
+ data-model-name="${modelName}"
121
+ title="${!versionId ? 'Download latest version' : 'Pre-fill Download Tab'}" >
122
+ <span class="base-model-badge">${baseModel}</span> ${versionName} <i class="fas fa-download"></i>
123
+ </button>
124
+ `;
125
+ }).join('')}
126
+ </div>
127
+ `;
128
+ }
129
+
130
+ let thumbnailHtml = '';
131
+ const videoTitle = `Video preview for ${modelName}`;
132
+ const imageAlt = `${modelName} thumbnail`;
133
+ if (thumbnailUrl && typeof thumbnailUrl === 'string' && thumbnailType === 'video') {
134
+ thumbnailHtml = `
135
+ <video class="huggingface-search-thumbnail" src="${thumbnailUrl}" autoplay loop muted playsinline
136
+ title="${videoTitle}"
137
+ onerror="console.error('Failed to load video preview:', this.src)">
138
+ Your browser does not support the video tag.
139
+ </video>
140
+ `;
141
+ } else {
142
+ const effective = thumbnailUrl || placeholder;
143
+ thumbnailHtml = `
144
+ <img src="${effective}" alt="${imageAlt}" class="huggingface-search-thumbnail" loading="lazy" onerror="${onErrorScript}">
145
+ `;
146
+ }
147
+
148
+ const overlayHtml = shouldBlur ? `<div class="huggingface-nsfw-overlay" title="R-rated: click to reveal">R</div>` : '';
149
+ const containerClasses = `huggingface-thumbnail-container${shouldBlur ? ' blurred' : ''}`;
150
+
151
+ listItem.innerHTML = `
152
+ <div class="${containerClasses}" data-nsfw-level="${nsfwLevel ?? ''}">
153
+ ${thumbnailHtml}
154
+ ${overlayHtml}
155
+ <div class="huggingface-type-badge" data-type="${modelTypeApi.toLowerCase()}">${modelTypeApi}</div>
156
+ </div>
157
+ <div class="huggingface-search-info">
158
+ <h4>${modelName}</h4>
159
+ <div class="huggingface-search-meta-info">
160
+ <span title="Creator: ${creator}"><i class="fas fa-user"></i> ${creator}</span>
161
+ <span title="Base Models: ${baseModelsDisplay}"><i class="fas fa-layer-group"></i> ${baseModelsDisplay}</span>
162
+ <span title="Published: ${lastUpdatedFormatted}"><i class="fas fa-calendar-alt"></i> ${lastUpdatedFormatted}</span>
163
+ </div>
164
+ <div class="huggingface-search-stats" title="Stats: Downloads / Rating (Count) / Likes">
165
+ <span title="Downloads"><i class="fas fa-download"></i> ${stats.downloadCount?.toLocaleString() || 0}</span>
166
+ <span title="Thumbs"><i class="fas fa-thumbs-up"></i> ${stats.thumbsUpCount?.toLocaleString() || 0}</span>
167
+ <span title="Collected"><i class="fas fa-archive"></i> ${stats.collectedCount?.toLocaleString() || 0}</span>
168
+ <span title="Buzz"><i class="fas fa-bolt"></i> ${stats.tippedAmountCount?.toLocaleString() || 0}</span>
169
+ </div>
170
+ ${tags.length > 0 ? `
171
+ <div class="huggingface-search-tags" title="${tags.join(', ')}">
172
+ ${tags.slice(0, 5).map(tag => `<span class="huggingface-search-tag">${tag}</span>`).join('')}
173
+ ${tags.length > 5 ? `<span class="huggingface-search-tag">...</span>` : ''}
174
+ </div>
175
+ ` : ''}
176
+ </div>
177
+ <div class="huggingface-search-actions">
178
+ <a href="https://huggingface.co/${baseRepoId}${primaryVersionId ? '?modelVersionId='+primaryVersionId : ''}"
179
+ target="_blank" rel="noopener noreferrer" class="huggingface-button small"
180
+ title="Open on HuggingFace website">
181
+ View <i class="fas fa-external-link-alt"></i>
182
+ </a>
183
+ <div class="version-buttons-container">
184
+ ${versionButtonsHtml}
185
+ ${moreButtonHtml}
186
+ </div>
187
+ ${allVersionsHtml}
188
+ </div>
189
+ `;
190
+
191
+ fragment.appendChild(listItem);
192
+ });
193
+
194
+ ui.searchResultsContainer.innerHTML = '';
195
+ ui.searchResultsContainer.appendChild(fragment);
196
+ }
custom_nodes/ComfyUI-HuggingFace/web/js/ui/statusRenderer.js ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Renders active/queued/history download lists
2
+
3
+ const PLACEHOLDER_IMAGE_URL = `/extensions/ComfyUI-HuggingFace/images/placeholder.jpg`;
4
+
5
+ export function renderDownloadList(ui, items, container, emptyMessage) {
6
+ if (!items || items.length === 0) {
7
+ container.innerHTML = `<p>${emptyMessage}</p>`;
8
+ return;
9
+ }
10
+
11
+ const fragment = document.createDocumentFragment();
12
+ items.forEach(item => {
13
+ const id = item.id || 'unknown-id';
14
+ const progress = item.progress !== undefined ? Math.max(0, Math.min(100, item.progress)) : 0;
15
+ const speed = item.speed !== undefined ? Math.max(0, item.speed) : 0;
16
+ const status = item.status || 'unknown';
17
+ const size = item.known_size !== undefined && item.known_size !== null ? item.known_size : (item.file_size || 0);
18
+ const downloadedBytes = size > 0 ? size * (progress / 100) : 0;
19
+ const errorMsg = item.error || null;
20
+
21
+ // Debug: log what we receive from backend
22
+ console.log(`[DEBUG Frontend] Item data:`, {
23
+ huggingface_model_name: item.huggingface_model_name,
24
+ model_name: item.model_name,
25
+ model: item.model,
26
+ version_name: item.version_name,
27
+ full_item: item // Log the full item to see all available fields
28
+ });
29
+
30
+ const modelName = item.huggingface_model_name || item.model_name || item.model?.name || 'Unknown Model';
31
+ console.log(`[DEBUG Frontend] Final modelName: ${modelName}`);
32
+ const versionName = item.version_name || 'Unknown Version';
33
+ const filename = item.filename || 'N/A';
34
+ const addedTime = item.added_time || null;
35
+ const startTime = item.start_time || null;
36
+ const endTime = item.end_time || null;
37
+ const thumbnail = item.thumbnail || PLACEHOLDER_IMAGE_URL;
38
+ const nsfwLevel = Number(item.thumbnail_nsfw_level ?? 0);
39
+ const blurMinLevel = Number(ui.settings?.nsfwBlurMinLevel ?? 4);
40
+ const shouldBlur = ui.settings?.hideMatureInSearch === true && nsfwLevel >= blurMinLevel;
41
+ const connectionType = item.connection_type || "N/A";
42
+
43
+ let progressBarClass = '';
44
+ let statusText = status.charAt(0).toUpperCase() + status.slice(1);
45
+ switch (status) {
46
+ case 'completed': progressBarClass = 'completed'; break;
47
+ case 'failed': progressBarClass = 'failed'; statusText = 'Failed'; break;
48
+ case 'cancelled': progressBarClass = 'cancelled'; statusText = 'Cancelled'; break;
49
+ case 'downloading': case 'queued': case 'starting': default: break;
50
+ }
51
+
52
+ const listItem = document.createElement('div');
53
+ listItem.className = 'huggingface-download-item';
54
+ listItem.dataset.id = id;
55
+
56
+ const onErrorScript = `this.onerror=null; this.src='${PLACEHOLDER_IMAGE_URL}'; this.style.backgroundColor='#444';`;
57
+ const addedTooltip = addedTime ? `data-tooltip="Added: ${new Date(addedTime).toLocaleString()}"` : '';
58
+ const startedTooltip = startTime ? `data-tooltip="Started: ${new Date(startTime).toLocaleString()}"` : '';
59
+ const endedTooltip = endTime ? `data-tooltip="Ended: ${new Date(endTime).toLocaleString()}"` : '';
60
+ const durationTooltip = startTime && endTime ? `data-tooltip="Duration: ${ui.formatDuration(startTime, endTime)}"` : '';
61
+ const filenameTooltip = filename !== 'N/A' ? `title="Filename: ${filename}"` : '';
62
+ const errorTooltip = errorMsg ? `title="Error Details: ${String(errorMsg).substring(0, 200)}${String(errorMsg).length > 200 ? '...' : ''}"` : '';
63
+ const connectionInfoHtml = connectionType !== "N/A" ? `<span style="font-size: 0.85em; color: #aaa; margin-left: 10px;">(Conn: ${connectionType})</span>` : '';
64
+
65
+ const overlayHtml = shouldBlur ? `<div class=\"huggingface-nsfw-overlay\" title=\"R-rated: click to reveal\">R</div>` : '';
66
+ const containerClasses = `huggingface-thumbnail-container${shouldBlur ? ' blurred' : ''}`;
67
+
68
+ let innerHTML = `
69
+ <div class="${containerClasses}" data-nsfw-level="${Number.isFinite(nsfwLevel) ? nsfwLevel : ''}">
70
+ <img src="${thumbnail}" alt="thumbnail" class="huggingface-download-thumbnail" loading="lazy" onerror="${onErrorScript}">
71
+ ${overlayHtml}
72
+ </div>
73
+ <div class="huggingface-download-info">
74
+ <strong>${modelName}</strong>
75
+ <p>Ver: ${versionName}</p>
76
+ <p class="filename" ${filenameTooltip}>${filename}</p>
77
+ ${size > 0 ? `<p>Size: ${ui.formatBytes(size)}</p>` : ''}
78
+ ${item.file_format ? `<p>Format: ${item.file_format}</p>` : ''}
79
+ ${item.file_precision || item.file_model_size ? `<p>${item.file_precision ? 'Precision: ' + String(item.file_precision).toUpperCase() : ''}${item.file_precision && item.file_model_size ? ' • ' : ''}${item.file_model_size ? 'Model Size: ' + item.file_model_size : ''}</p>` : ''}
80
+ ${errorMsg ? `<p class="error-message" ${errorTooltip}><i class="fas fa-exclamation-triangle"></i> ${String(errorMsg).substring(0, 100)}${String(errorMsg).length > 100 ? '...' : ''}</p>` : ''}
81
+ `;
82
+
83
+ if (status === 'downloading' || status === 'starting' || status === 'completed') {
84
+ const statusLine = `<div ${durationTooltip} ${endedTooltip}>Status: ${statusText} ${connectionInfoHtml}</div>`;
85
+ innerHTML += `
86
+ <div class="huggingface-progress-container" title="${statusText} - ${progress.toFixed(1)}%">
87
+ <div class="huggingface-progress-bar ${progressBarClass}" style="width: ${progress}%;">
88
+ ${progress > 15 ? progress.toFixed(0)+'%' : ''}
89
+ </div>
90
+ </div>
91
+ `;
92
+ const speedText = (status === 'downloading' && speed > 0) ? ui.formatSpeed(speed) : '';
93
+ const progressText = (status === 'downloading' && size > 0) ? `(${ui.formatBytes(downloadedBytes)} / ${ui.formatBytes(size)})` : '';
94
+ const completedText = status === 'completed' ? '' : '';
95
+ const speedProgLine = `<div class="huggingface-speed-indicator">${speedText} ${progressText} ${completedText}</div>`;
96
+ if (status === 'downloading') { innerHTML += speedProgLine; }
97
+ innerHTML += statusLine;
98
+ } else if (status === 'failed' || status === 'cancelled' || status === 'queued') {
99
+ innerHTML += `<div class="status-line-simple" ${durationTooltip} ${endedTooltip} ${addedTooltip}>Status: ${statusText} ${connectionInfoHtml}</div>`;
100
+ } else {
101
+ innerHTML += `<div class="status-line-simple">Status: ${statusText} ${connectionInfoHtml}</div>`;
102
+ }
103
+
104
+ innerHTML += `</div>`;
105
+ innerHTML += `<div class="huggingface-download-actions">`;
106
+ if (status === 'queued' || status === 'downloading' || status === 'starting') {
107
+ innerHTML += `<button class="huggingface-button danger small huggingface-cancel-button" data-id="${id}" title="Cancel Download"><i class="fas fa-times"></i></button>`;
108
+ }
109
+ if (status === 'failed' || status === 'cancelled') {
110
+ innerHTML += `<button class="huggingface-button small huggingface-retry-button" data-id="${id}" title="Retry Download"><i class="fas fa-redo"></i></button>`;
111
+ }
112
+ if (status === 'completed') {
113
+ innerHTML += `<button class="huggingface-button small huggingface-openpath-button" data-id="${id}" title="Open Containing Folder"><i class="fas fa-folder-open"></i></button>`;
114
+ }
115
+ innerHTML += `</div>`;
116
+
117
+ listItem.innerHTML = innerHTML;
118
+ fragment.appendChild(listItem);
119
+ });
120
+
121
+ container.innerHTML = '';
122
+ container.appendChild(fragment);
123
+ ui.ensureFontAwesome();
124
+ }
custom_nodes/ComfyUI-HuggingFace/web/js/ui/templates.js ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Modal template for HuggingFace UI
2
+ // Keep structure identical to the original inline HTML to minimize risk
3
+
4
+ export function modalTemplate(settings = {}) {
5
+ const numConnections = Number.isFinite(settings.numConnections) ? settings.numConnections : 1;
6
+ return `
7
+ <div class="huggingface-downloader-modal-content">
8
+ <div class="huggingface-downloader-header">
9
+ <h2>HuggingFace</h2>
10
+ <button class="huggingface-close-button" id="huggingface-close-modal">&times;</button>
11
+ </div>
12
+ <div class="huggingface-downloader-body">
13
+ <div class="huggingface-downloader-tabs">
14
+ <button class="huggingface-downloader-tab active" data-tab="download">Download</button>
15
+ <button class="huggingface-downloader-tab" data-tab="search">Search</button>
16
+ <button class="huggingface-downloader-tab" data-tab="status">Status <span id="huggingface-status-indicator" style="display: none;">(<span id="huggingface-active-count">0</span>)</span></button>
17
+ <button class="huggingface-downloader-tab" data-tab="settings">Settings</button>
18
+ </div>
19
+ <div id="huggingface-tab-download" class="huggingface-downloader-tab-content active">
20
+ <form id="huggingface-download-form">
21
+ <div class="huggingface-form-group">
22
+ <label for="huggingface-model-url">Model URL or ID</label>
23
+ <input type="text" id="huggingface-model-url" class="huggingface-input" placeholder="e.g., https://huggingface.com/models/12345 or 12345" required>
24
+ </div>
25
+ <p style="font-size: 0.9em; color: #ccc; margin-top: -10px; margin-bottom: 15px;">You can optionally specify a version ID using "?modelVersionId=xxxxx" in the URL or in the field below.</p>
26
+ <div class="huggingface-form-row">
27
+ <div class="huggingface-form-group">
28
+ <label for="huggingface-model-type">Model Type (Save Location)</label>
29
+ <div style="display:flex; gap:6px; align-items:center;">
30
+ <select id="huggingface-model-type" class="huggingface-select" required></select>
31
+ <button type="button" id="huggingface-create-model-type" class="huggingface-button small" title="Create new model type folder"><i class="fas fa-folder-plus"></i></button>
32
+ </div>
33
+ </div>
34
+ <div class="huggingface-form-group">
35
+ <label for="huggingface-subdir-select">Save Subfolder</label>
36
+ <div style="display:flex; gap:6px; align-items:center;">
37
+ <select id="huggingface-subdir-select" class="huggingface-select">
38
+ <option value="">(root)</option>
39
+ </select>
40
+ <button type="button" id="huggingface-create-subdir" class="huggingface-button small" title="Create new subfolder"><i class="fas fa-folder-plus"></i></button>
41
+ </div>
42
+ <p id="huggingface-save-base-path" style="font-size: 0.8em; color: #bbb; margin-top: 6px; word-break: break-all;"></p>
43
+ </div>
44
+ <div class="huggingface-form-group">
45
+ <label for="huggingface-model-version-id">Version ID (Optional)</label>
46
+ <input type="number" id="huggingface-model-version-id" class="huggingface-input" placeholder="Overrides URL/Latest">
47
+ </div>
48
+ </div>
49
+ <div class="huggingface-form-row">
50
+ <div class="huggingface-form-group">
51
+ <label for="huggingface-custom-filename">Custom Filename (Optional)</label>
52
+ <input type="text" id="huggingface-custom-filename" class="huggingface-input" placeholder="Leave blank to use original name">
53
+ </div>
54
+ <div class="huggingface-form-group">
55
+ <label for="huggingface-connections">Connections</label>
56
+ <input type="number" id="huggingface-connections" class="huggingface-input" value="${numConnections}" min="1" max="16" step="1" required disabled>
57
+ <p style="font-size: 0.9em; color: #ccc; margin-top: 7px; margin-bottom: 15px;">Disabled: Only single connection possible for now</p>
58
+ </div>
59
+ </div>
60
+ <div class="huggingface-form-group inline">
61
+ <input type="checkbox" id="huggingface-force-redownload" class="huggingface-checkbox">
62
+ <label for="huggingface-force-redownload">Force Re-download (if exists)</label>
63
+ </div>
64
+ <div id="huggingface-download-preview-area" class="huggingface-download-preview-area" style="margin-top: 25px; margin-bottom: 25px; padding-top: 15px; border-top: 1px solid var(--border-color, #444);">
65
+ <!-- Preview content will be injected here -->
66
+ </div>
67
+ <button type="submit" id="huggingface-download-submit" class="huggingface-button primary">Start Download</button>
68
+ </form>
69
+ </div>
70
+ <div id="huggingface-tab-search" class="huggingface-downloader-tab-content">
71
+ <form id="huggingface-search-form">
72
+ <div class="huggingface-search-controls">
73
+ <input type="text" id="huggingface-search-query" class="huggingface-input" placeholder="Search HuggingFace...">
74
+ <select id="huggingface-search-type" class="huggingface-select">
75
+ <option value="any">Any Type</option>
76
+ </select>
77
+ <select id="huggingface-search-base-model" class="huggingface-select">
78
+ <option value="any">Any Base Model</option>
79
+ </select>
80
+ <select id="huggingface-search-sort" class="huggingface-select">
81
+ <option value="Relevancy">Relevancy</option>
82
+ <option value="Highest Rated">Highest Rated</option>
83
+ <option value="Most Liked">Most Liked</option>
84
+ <option value="Most Discussed">Most Discussed</option>
85
+ <option value="Most Collected">Most Collected</option>
86
+ <option value="Most Buzz">Most Buzz</option>
87
+ <option value="Most Downloaded">Most Downloaded</option>
88
+ <option value="Newest">Newest</option>
89
+ </select>
90
+ </div>
91
+ <button type="submit" id="huggingface-search-submit" class="huggingface-button primary">Search</button>
92
+ </form>
93
+ <div id="huggingface-search-results" class="huggingface-search-results"></div>
94
+ <div id="huggingface-search-pagination" style="text-align: center; margin-top: 20px;"></div>
95
+ </div>
96
+ <div id="huggingface-tab-status" class="huggingface-downloader-tab-content">
97
+ <div id="huggingface-status-content">
98
+ <div class="huggingface-status-section">
99
+ <h3>Active Downloads</h3>
100
+ <div id="huggingface-active-list" class="huggingface-download-list">
101
+ <p>No active downloads.</p>
102
+ </div>
103
+ </div>
104
+ <div class="huggingface-status-section">
105
+ <h3>Queued Downloads</h3>
106
+ <div id="huggingface-queued-list" class="huggingface-download-list">
107
+ <p>Download queue is empty.</p>
108
+ </div>
109
+ </div>
110
+ <div class="huggingface-status-section">
111
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
112
+ <h3>Download History (Recent)</h3>
113
+ <button id="huggingface-clear-history-button" class="huggingface-button danger small" title="Clear all history items">
114
+ <i class="fas fa-trash-alt"></i> Clear History
115
+ </button>
116
+ </div>
117
+ <div id="huggingface-history-list" class="huggingface-download-list">
118
+ <p>No download history yet.</p>
119
+ </div>
120
+ </div>
121
+ </div>
122
+ </div>
123
+ <div id="huggingface-tab-settings" class="huggingface-downloader-tab-content">
124
+ <form id="huggingface-settings-form">
125
+ <div class="huggingface-settings-container">
126
+ <div class="huggingface-settings-section">
127
+ <h4>API & Defaults</h4>
128
+ <div class="huggingface-form-group">
129
+ <label for="huggingface-settings-api-key">HuggingFace HF token (Optional)</label>
130
+ <input type="password" id="huggingface-settings-api-key" class="huggingface-input" placeholder="Enter API key for higher limits / authenticated access" autocomplete="new-password">
131
+ <p style="font-size: 0.85em; color: #bbb; margin-top: 5px;">Needed for some downloads/features. Leave blank to use server env <code>HUGGINGFACE_TOKEN</code>. Find keys at huggingface.com/user/account</p>
132
+ </div>
133
+ <div class="huggingface-form-group">
134
+ <label for="huggingface-settings-global-root">Global Download Root (Optional)</label>
135
+ <input type="text" id="huggingface-settings-global-root" class="huggingface-input" placeholder="e.g., /runpod-volume/ComfyUI or F:/Models/ComfyUI">
136
+ <p style="font-size: 0.85em; color: #bbb; margin-top: 5px;">
137
+ When set, downloads use <code>&lt;global_root&gt;/&lt;model_type&gt;</code> (for example <code>/runpod-volume/ComfyUI/checkpoints</code>).
138
+ </p>
139
+ <div style="display:flex; gap:8px; margin-top: 8px; flex-wrap: wrap;">
140
+ <button type="button" id="huggingface-settings-set-global-root" class="huggingface-button small">Set Global Root</button>
141
+ <button type="button" id="huggingface-settings-clear-global-root" class="huggingface-button danger small">Clear Global Root</button>
142
+ </div>
143
+ </div>
144
+ <div class="huggingface-form-group">
145
+ <label for="huggingface-settings-connections">Default Connections</label>
146
+ <input type="number" id="huggingface-settings-connections" class="huggingface-input" value="4" min="1" max="16" step="1" required>
147
+ <p style="font-size: 0.85em; color: #bbb; margin-top: 5px;">Number of parallel connections for downloads (1-16)</p>
148
+ </div>
149
+ <div class="huggingface-form-group">
150
+ <label for="huggingface-settings-default-type">Default Model Type (for saving)</label>
151
+ <select id="huggingface-settings-default-type" class="huggingface-select" required></select>
152
+ </div>
153
+ </div>
154
+ <div class="huggingface-settings-section">
155
+ <h4>Interface & Search</h4>
156
+ <div class="huggingface-form-group inline">
157
+ <input type="checkbox" id="huggingface-settings-auto-open-status" class="huggingface-checkbox">
158
+ <label for="huggingface-settings-auto-open-status">Switch to Status tab after starting download</label>
159
+ </div>
160
+ <div class="huggingface-form-group inline">
161
+ <input type="checkbox" id="huggingface-settings-hide-mature" class="huggingface-checkbox" ${settings.hideMatureInSearch ? 'checked' : ''}>
162
+ <label for="huggingface-settings-hide-mature">Hide R-rated (Mature) images in search (click to reveal)</label>
163
+ </div>
164
+ <div class="huggingface-form-group">
165
+ <label for="huggingface-settings-nsfw-threshold">NSFW Blur Threshold (nsfwLevel)</label>
166
+ <input type="number" id="huggingface-settings-nsfw-threshold" class="huggingface-input" value="${Number.isFinite(settings.nsfwBlurMinLevel) ? settings.nsfwBlurMinLevel : 4}" min="0" max="128" step="1">
167
+ <p style="font-size: 0.85em; color: #bbb; margin-top: 5px;">
168
+ Blur thumbnails when an image's <code>nsfwLevel</code> is greater than or equal to this value.
169
+ Higher numbers indicate more explicit content. None (Safe/PG): 1, Mild (PG-13): 2, Mature (R): 4, Adult (X): 5, Extra Explicit (R): 8, Explicit (XXX): 16/32+
170
+ </p>
171
+ </div>
172
+ </div>
173
+ </div>
174
+ <button type="submit" id="huggingface-settings-save" class="huggingface-button primary" style="margin-top: 20px;">Save Settings</button>
175
+ </form>
176
+ </div>
177
+ </div>
178
+ <!-- Toast Notification Area -->
179
+ <div id="huggingface-toast" class="huggingface-toast"></div>
180
+ <!-- Confirmation Modal -->
181
+ <div id="huggingface-confirm-clear-modal" class="huggingface-confirmation-modal">
182
+ <div class="huggingface-confirmation-modal-content">
183
+ <h4>Confirm Clear History</h4>
184
+ <p>Are you sure you want to clear the download history? This action cannot be undone.</p>
185
+ <div class="huggingface-confirmation-modal-actions">
186
+ <button id="huggingface-confirm-clear-no" class="huggingface-button secondary">Cancel</button>
187
+ <button id="huggingface-confirm-clear-yes" class="huggingface-button danger">Confirm Clear</button>
188
+ </div>
189
+ </div>
190
+ </div>
191
+ </div>
192
+ `;
193
+ }
custom_nodes/ComfyUI-HuggingFace/web/js/utils/cookies.js ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Lightweight cookie helpers for HuggingFace UI
2
+ // Exports: setCookie, getCookie
3
+
4
+ export function setCookie(name, value, days) {
5
+ let expires = "";
6
+ if (days) {
7
+ const date = new Date();
8
+ date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
9
+ expires = "; expires=" + date.toUTCString();
10
+ }
11
+ document.cookie = `${name}=${value || ""}${expires}; path=/; SameSite=Lax`;
12
+ }
13
+
14
+ export function getCookie(name) {
15
+ const nameEQ = name + "=";
16
+ const parts = document.cookie.split(";");
17
+ for (let i = 0; i < parts.length; i++) {
18
+ let c = parts[i];
19
+ while (c.charAt(0) === " ") c = c.substring(1);
20
+ if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length);
21
+ }
22
+ return null;
23
+ }
24
+
custom_nodes/ComfyUI-HuggingFace/web/js/utils/dom.js ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // File: web/js/utils/dom.js
2
+
3
+ /**
4
+ * Dynamically adds a CSS link to the document's head.
5
+ * It resolves the path relative to this script's location using import.meta.url,
6
+ * making it robust against case-sensitivity issues and different install paths.
7
+ * @param {string} relativeHref - Relative path to the CSS file (e.g., '../huggingfaceDownloader.css').
8
+ * @param {string} [id="huggingface-downloader-styles"] - The ID for the link element.
9
+ */
10
+ export function addCssLink(relativeHref, id = "huggingface-downloader-styles") {
11
+ if (document.getElementById(id)) return; // Prevent duplicates
12
+
13
+ try {
14
+ const absoluteUrl = new URL(relativeHref, import.meta.url);
15
+
16
+ const link = document.createElement("link");
17
+ link.id = id;
18
+ link.rel = "stylesheet";
19
+ link.href = absoluteUrl.href;
20
+
21
+ link.onload = () => {
22
+ console.log("[HuggingFace] CSS loaded successfully:", link.href);
23
+ };
24
+ link.onerror = () => {
25
+ console.error("[HuggingFace] Critical error: Failed to load CSS from:", link.href);
26
+ };
27
+
28
+ document.head.appendChild(link);
29
+ } catch (e) {
30
+ console.error("[HuggingFace] Error creating CSS link. import.meta.url may be unsupported in this context.", e);
31
+ }
32
+ }
custom_nodes/ComfyUI-KJNodes/.github/workflows/publish.yml ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Publish to Comfy registry
2
+ on:
3
+ workflow_dispatch:
4
+ push:
5
+ branches:
6
+ - main
7
+ paths:
8
+ - "pyproject.toml"
9
+
10
+ permissions:
11
+ issues: write
12
+
13
+ jobs:
14
+ publish-node:
15
+ name: Publish Custom Node to registry
16
+ runs-on: ubuntu-latest
17
+ if: ${{ github.repository_owner == 'kijai' }}
18
+ steps:
19
+ - name: Check out code
20
+ uses: actions/checkout@v4
21
+ - name: Publish Custom Node
22
+ uses: Comfy-Org/publish-node-action@v1
23
+ with:
24
+ ## Add your own personal access token to your Github Repository secrets and reference it here.
25
+ personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
custom_nodes/ComfyUI-KJNodes/docs/images/319121566-05f66385-7568-4b1f-8bbc-11053660b02f.png ADDED
custom_nodes/ComfyUI-KJNodes/docs/images/319121636-706b5081-9120-4a29-bd76-901691ada688.png ADDED
custom_nodes/ComfyUI-KJNodes/example_workflows/leapfusion_hunyuuanvideo_i2v_native_testing.json ADDED
@@ -0,0 +1,1188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "last_node_id": 86,
3
+ "last_link_id": 144,
4
+ "nodes": [
5
+ {
6
+ "id": 62,
7
+ "type": "FluxGuidance",
8
+ "pos": [
9
+ -630,
10
+ -170
11
+ ],
12
+ "size": [
13
+ 317.4000244140625,
14
+ 58
15
+ ],
16
+ "flags": {},
17
+ "order": 13,
18
+ "mode": 0,
19
+ "inputs": [
20
+ {
21
+ "name": "conditioning",
22
+ "type": "CONDITIONING",
23
+ "link": 82
24
+ }
25
+ ],
26
+ "outputs": [
27
+ {
28
+ "name": "CONDITIONING",
29
+ "type": "CONDITIONING",
30
+ "links": [
31
+ 83
32
+ ],
33
+ "slot_index": 0
34
+ }
35
+ ],
36
+ "properties": {
37
+ "Node name for S&R": "FluxGuidance"
38
+ },
39
+ "widgets_values": [
40
+ 6
41
+ ]
42
+ },
43
+ {
44
+ "id": 51,
45
+ "type": "KSamplerSelect",
46
+ "pos": [
47
+ -610,
48
+ -480
49
+ ],
50
+ "size": [
51
+ 315,
52
+ 58
53
+ ],
54
+ "flags": {},
55
+ "order": 0,
56
+ "mode": 0,
57
+ "inputs": [],
58
+ "outputs": [
59
+ {
60
+ "name": "SAMPLER",
61
+ "type": "SAMPLER",
62
+ "links": [
63
+ 61
64
+ ]
65
+ }
66
+ ],
67
+ "properties": {
68
+ "Node name for S&R": "KSamplerSelect"
69
+ },
70
+ "widgets_values": [
71
+ "euler"
72
+ ]
73
+ },
74
+ {
75
+ "id": 57,
76
+ "type": "VAEDecodeTiled",
77
+ "pos": [
78
+ -200,
79
+ 90
80
+ ],
81
+ "size": [
82
+ 315,
83
+ 150
84
+ ],
85
+ "flags": {},
86
+ "order": 20,
87
+ "mode": 0,
88
+ "inputs": [
89
+ {
90
+ "name": "samples",
91
+ "type": "LATENT",
92
+ "link": 142
93
+ },
94
+ {
95
+ "name": "vae",
96
+ "type": "VAE",
97
+ "link": 74
98
+ }
99
+ ],
100
+ "outputs": [
101
+ {
102
+ "name": "IMAGE",
103
+ "type": "IMAGE",
104
+ "links": [
105
+ 105
106
+ ],
107
+ "slot_index": 0
108
+ }
109
+ ],
110
+ "properties": {
111
+ "Node name for S&R": "VAEDecodeTiled"
112
+ },
113
+ "widgets_values": [
114
+ 128,
115
+ 64,
116
+ 64,
117
+ 8
118
+ ]
119
+ },
120
+ {
121
+ "id": 65,
122
+ "type": "LoadImage",
123
+ "pos": [
124
+ -2212.498779296875,
125
+ -632.4085083007812
126
+ ],
127
+ "size": [
128
+ 315,
129
+ 314
130
+ ],
131
+ "flags": {},
132
+ "order": 1,
133
+ "mode": 0,
134
+ "inputs": [],
135
+ "outputs": [
136
+ {
137
+ "name": "IMAGE",
138
+ "type": "IMAGE",
139
+ "links": [
140
+ 86
141
+ ],
142
+ "slot_index": 0
143
+ },
144
+ {
145
+ "name": "MASK",
146
+ "type": "MASK",
147
+ "links": null
148
+ }
149
+ ],
150
+ "properties": {
151
+ "Node name for S&R": "LoadImage"
152
+ },
153
+ "widgets_values": [
154
+ "Mona-Lisa-oil-wood-panel-Leonardo-da.webp",
155
+ "image"
156
+ ]
157
+ },
158
+ {
159
+ "id": 64,
160
+ "type": "VAEEncode",
161
+ "pos": [
162
+ -1336.7884521484375,
163
+ -492.5806884765625
164
+ ],
165
+ "size": [
166
+ 210,
167
+ 46
168
+ ],
169
+ "flags": {},
170
+ "order": 14,
171
+ "mode": 0,
172
+ "inputs": [
173
+ {
174
+ "name": "pixels",
175
+ "type": "IMAGE",
176
+ "link": 144
177
+ },
178
+ {
179
+ "name": "vae",
180
+ "type": "VAE",
181
+ "link": 88
182
+ }
183
+ ],
184
+ "outputs": [
185
+ {
186
+ "name": "LATENT",
187
+ "type": "LATENT",
188
+ "links": [
189
+ 137
190
+ ],
191
+ "slot_index": 0
192
+ }
193
+ ],
194
+ "properties": {
195
+ "Node name for S&R": "VAEEncode"
196
+ },
197
+ "widgets_values": []
198
+ },
199
+ {
200
+ "id": 44,
201
+ "type": "UNETLoader",
202
+ "pos": [
203
+ -2373.55029296875,
204
+ -193.91510009765625
205
+ ],
206
+ "size": [
207
+ 459.56060791015625,
208
+ 82
209
+ ],
210
+ "flags": {},
211
+ "order": 2,
212
+ "mode": 0,
213
+ "inputs": [],
214
+ "outputs": [
215
+ {
216
+ "name": "MODEL",
217
+ "type": "MODEL",
218
+ "links": [
219
+ 135
220
+ ],
221
+ "slot_index": 0
222
+ }
223
+ ],
224
+ "properties": {
225
+ "Node name for S&R": "UNETLoader"
226
+ },
227
+ "widgets_values": [
228
+ "hyvideo\\hunyuan_video_720_fp8_e4m3fn.safetensors",
229
+ "fp8_e4m3fn_fast"
230
+ ]
231
+ },
232
+ {
233
+ "id": 49,
234
+ "type": "VAELoader",
235
+ "pos": [
236
+ -1876.39306640625,
237
+ -35.19633865356445
238
+ ],
239
+ "size": [
240
+ 433.7603454589844,
241
+ 58.71116256713867
242
+ ],
243
+ "flags": {},
244
+ "order": 3,
245
+ "mode": 0,
246
+ "inputs": [],
247
+ "outputs": [
248
+ {
249
+ "name": "VAE",
250
+ "type": "VAE",
251
+ "links": [
252
+ 74,
253
+ 88
254
+ ],
255
+ "slot_index": 0
256
+ }
257
+ ],
258
+ "properties": {
259
+ "Node name for S&R": "VAELoader"
260
+ },
261
+ "widgets_values": [
262
+ "hyvid\\hunyuan_video_vae_bf16.safetensors"
263
+ ]
264
+ },
265
+ {
266
+ "id": 47,
267
+ "type": "DualCLIPLoader",
268
+ "pos": [
269
+ -2284.893798828125,
270
+ 150.4042205810547
271
+ ],
272
+ "size": [
273
+ 343.3958435058594,
274
+ 106.86042785644531
275
+ ],
276
+ "flags": {},
277
+ "order": 4,
278
+ "mode": 0,
279
+ "inputs": [],
280
+ "outputs": [
281
+ {
282
+ "name": "CLIP",
283
+ "type": "CLIP",
284
+ "links": [
285
+ 56
286
+ ],
287
+ "slot_index": 0
288
+ }
289
+ ],
290
+ "properties": {
291
+ "Node name for S&R": "DualCLIPLoader"
292
+ },
293
+ "widgets_values": [
294
+ "clip_l.safetensors",
295
+ "llava_llama3_fp16.safetensors",
296
+ "hunyuan_video",
297
+ "default"
298
+ ]
299
+ },
300
+ {
301
+ "id": 45,
302
+ "type": "CLIPTextEncode",
303
+ "pos": [
304
+ -1839.1649169921875,
305
+ 143.5203094482422
306
+ ],
307
+ "size": [
308
+ 400,
309
+ 200
310
+ ],
311
+ "flags": {},
312
+ "order": 8,
313
+ "mode": 0,
314
+ "inputs": [
315
+ {
316
+ "name": "clip",
317
+ "type": "CLIP",
318
+ "link": 56
319
+ }
320
+ ],
321
+ "outputs": [
322
+ {
323
+ "name": "CONDITIONING",
324
+ "type": "CONDITIONING",
325
+ "links": [
326
+ 69,
327
+ 82
328
+ ],
329
+ "slot_index": 0
330
+ }
331
+ ],
332
+ "properties": {
333
+ "Node name for S&R": "CLIPTextEncode"
334
+ },
335
+ "widgets_values": [
336
+ "woman puts on sunglasses"
337
+ ]
338
+ },
339
+ {
340
+ "id": 53,
341
+ "type": "EmptyHunyuanLatentVideo",
342
+ "pos": [
343
+ -1120,
344
+ 90
345
+ ],
346
+ "size": [
347
+ 315,
348
+ 130
349
+ ],
350
+ "flags": {},
351
+ "order": 10,
352
+ "mode": 0,
353
+ "inputs": [
354
+ {
355
+ "name": "width",
356
+ "type": "INT",
357
+ "link": 89,
358
+ "widget": {
359
+ "name": "width"
360
+ }
361
+ },
362
+ {
363
+ "name": "height",
364
+ "type": "INT",
365
+ "link": 90,
366
+ "widget": {
367
+ "name": "height"
368
+ }
369
+ }
370
+ ],
371
+ "outputs": [
372
+ {
373
+ "name": "LATENT",
374
+ "type": "LATENT",
375
+ "links": [
376
+ 119
377
+ ],
378
+ "slot_index": 0
379
+ }
380
+ ],
381
+ "properties": {
382
+ "Node name for S&R": "EmptyHunyuanLatentVideo"
383
+ },
384
+ "widgets_values": [
385
+ 960,
386
+ 544,
387
+ 65,
388
+ 1
389
+ ]
390
+ },
391
+ {
392
+ "id": 55,
393
+ "type": "ConditioningZeroOut",
394
+ "pos": [
395
+ -910,
396
+ 300
397
+ ],
398
+ "size": [
399
+ 251.14309692382812,
400
+ 26
401
+ ],
402
+ "flags": {
403
+ "collapsed": true
404
+ },
405
+ "order": 12,
406
+ "mode": 0,
407
+ "inputs": [
408
+ {
409
+ "name": "conditioning",
410
+ "type": "CONDITIONING",
411
+ "link": 69
412
+ }
413
+ ],
414
+ "outputs": [
415
+ {
416
+ "name": "CONDITIONING",
417
+ "type": "CONDITIONING",
418
+ "links": [
419
+ 70
420
+ ],
421
+ "slot_index": 0
422
+ }
423
+ ],
424
+ "properties": {
425
+ "Node name for S&R": "ConditioningZeroOut"
426
+ },
427
+ "widgets_values": []
428
+ },
429
+ {
430
+ "id": 52,
431
+ "type": "BasicScheduler",
432
+ "pos": [
433
+ -600,
434
+ -350
435
+ ],
436
+ "size": [
437
+ 315,
438
+ 106
439
+ ],
440
+ "flags": {},
441
+ "order": 17,
442
+ "mode": 0,
443
+ "inputs": [
444
+ {
445
+ "name": "model",
446
+ "type": "MODEL",
447
+ "link": 78
448
+ }
449
+ ],
450
+ "outputs": [
451
+ {
452
+ "name": "SIGMAS",
453
+ "type": "SIGMAS",
454
+ "links": [
455
+ 62
456
+ ],
457
+ "slot_index": 0
458
+ }
459
+ ],
460
+ "properties": {
461
+ "Node name for S&R": "BasicScheduler"
462
+ },
463
+ "widgets_values": [
464
+ "simple",
465
+ 20,
466
+ 1
467
+ ]
468
+ },
469
+ {
470
+ "id": 42,
471
+ "type": "SamplerCustom",
472
+ "pos": [
473
+ -640,
474
+ 10
475
+ ],
476
+ "size": [
477
+ 355.20001220703125,
478
+ 467.4666748046875
479
+ ],
480
+ "flags": {},
481
+ "order": 18,
482
+ "mode": 0,
483
+ "inputs": [
484
+ {
485
+ "name": "model",
486
+ "type": "MODEL",
487
+ "link": 77
488
+ },
489
+ {
490
+ "name": "positive",
491
+ "type": "CONDITIONING",
492
+ "link": 83
493
+ },
494
+ {
495
+ "name": "negative",
496
+ "type": "CONDITIONING",
497
+ "link": 70
498
+ },
499
+ {
500
+ "name": "sampler",
501
+ "type": "SAMPLER",
502
+ "link": 61
503
+ },
504
+ {
505
+ "name": "sigmas",
506
+ "type": "SIGMAS",
507
+ "link": 62
508
+ },
509
+ {
510
+ "name": "latent_image",
511
+ "type": "LATENT",
512
+ "link": 119
513
+ }
514
+ ],
515
+ "outputs": [
516
+ {
517
+ "name": "output",
518
+ "type": "LATENT",
519
+ "links": null
520
+ },
521
+ {
522
+ "name": "denoised_output",
523
+ "type": "LATENT",
524
+ "links": [
525
+ 141
526
+ ],
527
+ "slot_index": 1
528
+ }
529
+ ],
530
+ "properties": {
531
+ "Node name for S&R": "SamplerCustom"
532
+ },
533
+ "widgets_values": [
534
+ true,
535
+ 6,
536
+ "fixed",
537
+ 1,
538
+ null
539
+ ]
540
+ },
541
+ {
542
+ "id": 84,
543
+ "type": "GetLatentRangeFromBatch",
544
+ "pos": [
545
+ -240,
546
+ -100
547
+ ],
548
+ "size": [
549
+ 340.20001220703125,
550
+ 82
551
+ ],
552
+ "flags": {},
553
+ "order": 19,
554
+ "mode": 0,
555
+ "inputs": [
556
+ {
557
+ "name": "latents",
558
+ "type": "LATENT",
559
+ "link": 141
560
+ }
561
+ ],
562
+ "outputs": [
563
+ {
564
+ "name": "LATENT",
565
+ "type": "LATENT",
566
+ "links": [
567
+ 142
568
+ ],
569
+ "slot_index": 0
570
+ }
571
+ ],
572
+ "properties": {
573
+ "Node name for S&R": "GetLatentRangeFromBatch"
574
+ },
575
+ "widgets_values": [
576
+ 1,
577
+ -1
578
+ ]
579
+ },
580
+ {
581
+ "id": 50,
582
+ "type": "VHS_VideoCombine",
583
+ "pos": [
584
+ 165.77645874023438,
585
+ -619.0606079101562
586
+ ],
587
+ "size": [
588
+ 1112.6898193359375,
589
+ 1076.4598388671875
590
+ ],
591
+ "flags": {},
592
+ "order": 21,
593
+ "mode": 0,
594
+ "inputs": [
595
+ {
596
+ "name": "images",
597
+ "type": "IMAGE",
598
+ "link": 105
599
+ },
600
+ {
601
+ "name": "audio",
602
+ "type": "AUDIO",
603
+ "link": null,
604
+ "shape": 7
605
+ },
606
+ {
607
+ "name": "meta_batch",
608
+ "type": "VHS_BatchManager",
609
+ "link": null,
610
+ "shape": 7
611
+ },
612
+ {
613
+ "name": "vae",
614
+ "type": "VAE",
615
+ "link": null,
616
+ "shape": 7
617
+ }
618
+ ],
619
+ "outputs": [
620
+ {
621
+ "name": "Filenames",
622
+ "type": "VHS_FILENAMES",
623
+ "links": null
624
+ }
625
+ ],
626
+ "properties": {
627
+ "Node name for S&R": "VHS_VideoCombine"
628
+ },
629
+ "widgets_values": {
630
+ "frame_rate": 24,
631
+ "loop_count": 0,
632
+ "filename_prefix": "hyvidcomfy",
633
+ "format": "video/h264-mp4",
634
+ "pix_fmt": "yuv420p",
635
+ "crf": 19,
636
+ "save_metadata": true,
637
+ "trim_to_audio": false,
638
+ "pingpong": false,
639
+ "save_output": false,
640
+ "videopreview": {
641
+ "hidden": false,
642
+ "paused": false,
643
+ "params": {
644
+ "filename": "hyvidcomfy_00001.mp4",
645
+ "subfolder": "",
646
+ "type": "temp",
647
+ "format": "video/h264-mp4",
648
+ "frame_rate": 24,
649
+ "workflow": "hyvidcomfy_00001.png",
650
+ "fullpath": "N:\\AI\\ComfyUI\\temp\\hyvidcomfy_00001.mp4"
651
+ },
652
+ "muted": false
653
+ }
654
+ }
655
+ },
656
+ {
657
+ "id": 54,
658
+ "type": "ModelSamplingSD3",
659
+ "pos": [
660
+ -1079.9112548828125,
661
+ -146.69448852539062
662
+ ],
663
+ "size": [
664
+ 315,
665
+ 58
666
+ ],
667
+ "flags": {},
668
+ "order": 16,
669
+ "mode": 0,
670
+ "inputs": [
671
+ {
672
+ "name": "model",
673
+ "type": "MODEL",
674
+ "link": 117
675
+ }
676
+ ],
677
+ "outputs": [
678
+ {
679
+ "name": "MODEL",
680
+ "type": "MODEL",
681
+ "links": [
682
+ 77,
683
+ 78
684
+ ],
685
+ "slot_index": 0
686
+ }
687
+ ],
688
+ "properties": {
689
+ "Node name for S&R": "ModelSamplingSD3"
690
+ },
691
+ "widgets_values": [
692
+ 9
693
+ ]
694
+ },
695
+ {
696
+ "id": 80,
697
+ "type": "PathchSageAttentionKJ",
698
+ "pos": [
699
+ -2273.926513671875,
700
+ -36.720542907714844
701
+ ],
702
+ "size": [
703
+ 315,
704
+ 58
705
+ ],
706
+ "flags": {},
707
+ "order": 7,
708
+ "mode": 4,
709
+ "inputs": [
710
+ {
711
+ "name": "model",
712
+ "type": "MODEL",
713
+ "link": 135
714
+ }
715
+ ],
716
+ "outputs": [
717
+ {
718
+ "name": "MODEL",
719
+ "type": "MODEL",
720
+ "links": [
721
+ 136
722
+ ],
723
+ "slot_index": 0
724
+ }
725
+ ],
726
+ "properties": {
727
+ "Node name for S&R": "PathchSageAttentionKJ"
728
+ },
729
+ "widgets_values": [
730
+ "auto"
731
+ ]
732
+ },
733
+ {
734
+ "id": 85,
735
+ "type": "Note",
736
+ "pos": [
737
+ -1838.572265625,
738
+ -302.1575927734375
739
+ ],
740
+ "size": [
741
+ 408.4594421386719,
742
+ 58
743
+ ],
744
+ "flags": {},
745
+ "order": 5,
746
+ "mode": 0,
747
+ "inputs": [],
748
+ "outputs": [],
749
+ "properties": {},
750
+ "widgets_values": [
751
+ "https://huggingface.co/Kijai/Leapfusion-image2vid-comfy/blob/main/leapfusion_img2vid544p_comfy.safetensors"
752
+ ],
753
+ "color": "#432",
754
+ "bgcolor": "#653"
755
+ },
756
+ {
757
+ "id": 74,
758
+ "type": "LeapfusionHunyuanI2VPatcher",
759
+ "pos": [
760
+ -1059.552978515625,
761
+ -459.34674072265625
762
+ ],
763
+ "size": [
764
+ 277.3238525390625,
765
+ 150
766
+ ],
767
+ "flags": {},
768
+ "order": 15,
769
+ "mode": 0,
770
+ "inputs": [
771
+ {
772
+ "name": "model",
773
+ "type": "MODEL",
774
+ "link": 123
775
+ },
776
+ {
777
+ "name": "latent",
778
+ "type": "LATENT",
779
+ "link": 137
780
+ }
781
+ ],
782
+ "outputs": [
783
+ {
784
+ "name": "MODEL",
785
+ "type": "MODEL",
786
+ "links": [
787
+ 117
788
+ ],
789
+ "slot_index": 0
790
+ }
791
+ ],
792
+ "properties": {
793
+ "Node name for S&R": "LeapfusionHunyuanI2VPatcher"
794
+ },
795
+ "widgets_values": [
796
+ 0,
797
+ 0,
798
+ 1,
799
+ 0.8
800
+ ]
801
+ },
802
+ {
803
+ "id": 59,
804
+ "type": "LoraLoaderModelOnly",
805
+ "pos": [
806
+ -1870.3748779296875,
807
+ -194.6091766357422
808
+ ],
809
+ "size": [
810
+ 442.8438720703125,
811
+ 82
812
+ ],
813
+ "flags": {},
814
+ "order": 11,
815
+ "mode": 0,
816
+ "inputs": [
817
+ {
818
+ "name": "model",
819
+ "type": "MODEL",
820
+ "link": 136
821
+ }
822
+ ],
823
+ "outputs": [
824
+ {
825
+ "name": "MODEL",
826
+ "type": "MODEL",
827
+ "links": [
828
+ 123
829
+ ],
830
+ "slot_index": 0
831
+ }
832
+ ],
833
+ "properties": {
834
+ "Node name for S&R": "LoraLoaderModelOnly"
835
+ },
836
+ "widgets_values": [
837
+ "hyvid\\musubi-tuner\\img2vid544p.safetensors",
838
+ 1
839
+ ]
840
+ },
841
+ {
842
+ "id": 66,
843
+ "type": "ImageResizeKJ",
844
+ "pos": [
845
+ -1821.1531982421875,
846
+ -632.925048828125
847
+ ],
848
+ "size": [
849
+ 315,
850
+ 266
851
+ ],
852
+ "flags": {},
853
+ "order": 6,
854
+ "mode": 0,
855
+ "inputs": [
856
+ {
857
+ "name": "image",
858
+ "type": "IMAGE",
859
+ "link": 86
860
+ },
861
+ {
862
+ "name": "get_image_size",
863
+ "type": "IMAGE",
864
+ "link": null,
865
+ "shape": 7
866
+ },
867
+ {
868
+ "name": "width_input",
869
+ "type": "INT",
870
+ "link": null,
871
+ "widget": {
872
+ "name": "width_input"
873
+ },
874
+ "shape": 7
875
+ },
876
+ {
877
+ "name": "height_input",
878
+ "type": "INT",
879
+ "link": null,
880
+ "widget": {
881
+ "name": "height_input"
882
+ },
883
+ "shape": 7
884
+ }
885
+ ],
886
+ "outputs": [
887
+ {
888
+ "name": "IMAGE",
889
+ "type": "IMAGE",
890
+ "links": [
891
+ 143
892
+ ],
893
+ "slot_index": 0
894
+ },
895
+ {
896
+ "name": "width",
897
+ "type": "INT",
898
+ "links": [
899
+ 89
900
+ ],
901
+ "slot_index": 1
902
+ },
903
+ {
904
+ "name": "height",
905
+ "type": "INT",
906
+ "links": [
907
+ 90
908
+ ],
909
+ "slot_index": 2
910
+ }
911
+ ],
912
+ "properties": {
913
+ "Node name for S&R": "ImageResizeKJ"
914
+ },
915
+ "widgets_values": [
916
+ 960,
917
+ 640,
918
+ "lanczos",
919
+ false,
920
+ 2,
921
+ 0,
922
+ 0,
923
+ "center"
924
+ ]
925
+ },
926
+ {
927
+ "id": 86,
928
+ "type": "ImageNoiseAugmentation",
929
+ "pos": [
930
+ -1361.111572265625,
931
+ -667.0104370117188
932
+ ],
933
+ "size": [
934
+ 315,
935
+ 106
936
+ ],
937
+ "flags": {},
938
+ "order": 9,
939
+ "mode": 0,
940
+ "inputs": [
941
+ {
942
+ "name": "image",
943
+ "type": "IMAGE",
944
+ "link": 143
945
+ }
946
+ ],
947
+ "outputs": [
948
+ {
949
+ "name": "IMAGE",
950
+ "type": "IMAGE",
951
+ "links": [
952
+ 144
953
+ ],
954
+ "slot_index": 0
955
+ }
956
+ ],
957
+ "properties": {
958
+ "Node name for S&R": "ImageNoiseAugmentation"
959
+ },
960
+ "widgets_values": [
961
+ 0.05,
962
+ 123,
963
+ "fixed"
964
+ ]
965
+ }
966
+ ],
967
+ "links": [
968
+ [
969
+ 56,
970
+ 47,
971
+ 0,
972
+ 45,
973
+ 0,
974
+ "CLIP"
975
+ ],
976
+ [
977
+ 61,
978
+ 51,
979
+ 0,
980
+ 42,
981
+ 3,
982
+ "SAMPLER"
983
+ ],
984
+ [
985
+ 62,
986
+ 52,
987
+ 0,
988
+ 42,
989
+ 4,
990
+ "SIGMAS"
991
+ ],
992
+ [
993
+ 69,
994
+ 45,
995
+ 0,
996
+ 55,
997
+ 0,
998
+ "CONDITIONING"
999
+ ],
1000
+ [
1001
+ 70,
1002
+ 55,
1003
+ 0,
1004
+ 42,
1005
+ 2,
1006
+ "CONDITIONING"
1007
+ ],
1008
+ [
1009
+ 74,
1010
+ 49,
1011
+ 0,
1012
+ 57,
1013
+ 1,
1014
+ "VAE"
1015
+ ],
1016
+ [
1017
+ 77,
1018
+ 54,
1019
+ 0,
1020
+ 42,
1021
+ 0,
1022
+ "MODEL"
1023
+ ],
1024
+ [
1025
+ 78,
1026
+ 54,
1027
+ 0,
1028
+ 52,
1029
+ 0,
1030
+ "MODEL"
1031
+ ],
1032
+ [
1033
+ 82,
1034
+ 45,
1035
+ 0,
1036
+ 62,
1037
+ 0,
1038
+ "CONDITIONING"
1039
+ ],
1040
+ [
1041
+ 83,
1042
+ 62,
1043
+ 0,
1044
+ 42,
1045
+ 1,
1046
+ "CONDITIONING"
1047
+ ],
1048
+ [
1049
+ 86,
1050
+ 65,
1051
+ 0,
1052
+ 66,
1053
+ 0,
1054
+ "IMAGE"
1055
+ ],
1056
+ [
1057
+ 88,
1058
+ 49,
1059
+ 0,
1060
+ 64,
1061
+ 1,
1062
+ "VAE"
1063
+ ],
1064
+ [
1065
+ 89,
1066
+ 66,
1067
+ 1,
1068
+ 53,
1069
+ 0,
1070
+ "INT"
1071
+ ],
1072
+ [
1073
+ 90,
1074
+ 66,
1075
+ 2,
1076
+ 53,
1077
+ 1,
1078
+ "INT"
1079
+ ],
1080
+ [
1081
+ 105,
1082
+ 57,
1083
+ 0,
1084
+ 50,
1085
+ 0,
1086
+ "IMAGE"
1087
+ ],
1088
+ [
1089
+ 117,
1090
+ 74,
1091
+ 0,
1092
+ 54,
1093
+ 0,
1094
+ "MODEL"
1095
+ ],
1096
+ [
1097
+ 119,
1098
+ 53,
1099
+ 0,
1100
+ 42,
1101
+ 5,
1102
+ "LATENT"
1103
+ ],
1104
+ [
1105
+ 123,
1106
+ 59,
1107
+ 0,
1108
+ 74,
1109
+ 0,
1110
+ "MODEL"
1111
+ ],
1112
+ [
1113
+ 135,
1114
+ 44,
1115
+ 0,
1116
+ 80,
1117
+ 0,
1118
+ "MODEL"
1119
+ ],
1120
+ [
1121
+ 136,
1122
+ 80,
1123
+ 0,
1124
+ 59,
1125
+ 0,
1126
+ "MODEL"
1127
+ ],
1128
+ [
1129
+ 137,
1130
+ 64,
1131
+ 0,
1132
+ 74,
1133
+ 1,
1134
+ "LATENT"
1135
+ ],
1136
+ [
1137
+ 141,
1138
+ 42,
1139
+ 1,
1140
+ 84,
1141
+ 0,
1142
+ "LATENT"
1143
+ ],
1144
+ [
1145
+ 142,
1146
+ 84,
1147
+ 0,
1148
+ 57,
1149
+ 0,
1150
+ "LATENT"
1151
+ ],
1152
+ [
1153
+ 143,
1154
+ 66,
1155
+ 0,
1156
+ 86,
1157
+ 0,
1158
+ "IMAGE"
1159
+ ],
1160
+ [
1161
+ 144,
1162
+ 86,
1163
+ 0,
1164
+ 64,
1165
+ 0,
1166
+ "IMAGE"
1167
+ ]
1168
+ ],
1169
+ "groups": [],
1170
+ "config": {},
1171
+ "extra": {
1172
+ "ds": {
1173
+ "scale": 0.740024994425854,
1174
+ "offset": [
1175
+ 2525.036093151529,
1176
+ 802.59123935694
1177
+ ]
1178
+ },
1179
+ "node_versions": {
1180
+ "comfy-core": "0.3.13",
1181
+ "ComfyUI-KJNodes": "a8aeef670b3f288303f956bf94385cb87978ea93",
1182
+ "ComfyUI-VideoHelperSuite": "c47b10ca1798b4925ff5a5f07d80c51ca80a837d"
1183
+ },
1184
+ "VHS_latentpreview": true,
1185
+ "VHS_latentpreviewrate": 0
1186
+ },
1187
+ "version": 0.4
1188
+ }