Instructions to use saik0s/comfy_backup with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- llama-cpp-python
How to use saik0s/comfy_backup with llama-cpp-python:
# !pip install llama-cpp-python from llama_cpp import Llama llm = Llama.from_pretrained( repo_id="saik0s/comfy_backup", filename="models/text_encoders/Qwen3VL-8B-Uncensored-HauhauCS-Aggressive-Q8_0.gguf", )
llm.create_chat_completion( messages = "No input example has been defined for this model task." )
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- llama.cpp
How to use saik0s/comfy_backup with llama.cpp:
Install (macOS, Linux)
curl -LsSf https://llama.app/install.sh | sh # Start a local OpenAI-compatible server with a web UI: llama serve -hf saik0s/comfy_backup:Q8_0 # Run inference directly in the terminal: llama cli -hf saik0s/comfy_backup:Q8_0
Install from WinGet (Windows)
winget install llama.cpp # Start a local OpenAI-compatible server with a web UI: llama serve -hf saik0s/comfy_backup:Q8_0 # Run inference directly in the terminal: llama cli -hf saik0s/comfy_backup:Q8_0
Use pre-built binary
# Download pre-built binary from: # https://github.com/ggerganov/llama.cpp/releases # Start a local OpenAI-compatible server with a web UI: ./llama-server -hf saik0s/comfy_backup:Q8_0 # Run inference directly in the terminal: ./llama-cli -hf saik0s/comfy_backup:Q8_0
Build from source code
git clone https://github.com/ggerganov/llama.cpp.git cd llama.cpp cmake -B build cmake --build build -j --target llama-server llama-cli # Start a local OpenAI-compatible server with a web UI: ./build/bin/llama-server -hf saik0s/comfy_backup:Q8_0 # Run inference directly in the terminal: ./build/bin/llama-cli -hf saik0s/comfy_backup:Q8_0
Use Docker
docker model run hf.co/saik0s/comfy_backup:Q8_0
- LM Studio
- Jan
- Ollama
How to use saik0s/comfy_backup with Ollama:
ollama run hf.co/saik0s/comfy_backup:Q8_0
- Unsloth Studio
How to use saik0s/comfy_backup with Unsloth Studio:
Install Unsloth Studio (macOS, Linux, WSL)
curl -fsSL https://unsloth.ai/install.sh | sh # Run unsloth studio unsloth studio -H 0.0.0.0 -p 8888 # Then open http://localhost:8888 in your browser # Search for saik0s/comfy_backup to start chatting
Install Unsloth Studio (Windows)
irm https://unsloth.ai/install.ps1 | iex # Run unsloth studio unsloth studio -H 0.0.0.0 -p 8888 # Then open http://localhost:8888 in your browser # Search for saik0s/comfy_backup to start chatting
Using HuggingFace Spaces for Unsloth
# No setup required # Open https://huggingface.co/spaces/unsloth/studio in your browser # Search for saik0s/comfy_backup to start chatting
- Pi
How to use saik0s/comfy_backup with Pi:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf saik0s/comfy_backup:Q8_0
Configure the model in Pi
# Install Pi: npm install -g @mariozechner/pi-coding-agent # Add to ~/.pi/agent/models.json: { "providers": { "llama-cpp": { "baseUrl": "http://localhost:8080/v1", "api": "openai-completions", "apiKey": "none", "models": [ { "id": "saik0s/comfy_backup:Q8_0" } ] } } }Run Pi
# Start Pi in your project directory: pi
- Hermes Agent new
How to use saik0s/comfy_backup with Hermes Agent:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf saik0s/comfy_backup:Q8_0
Configure Hermes
# Install Hermes: curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash hermes setup # Point Hermes at the local server: hermes config set model.provider custom hermes config set model.base_url http://127.0.0.1:8080/v1 hermes config set model.default saik0s/comfy_backup:Q8_0
Run Hermes
hermes
- Atomic Chat new
- OpenClaw new
How to use saik0s/comfy_backup with OpenClaw:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf saik0s/comfy_backup:Q8_0
Configure OpenClaw
# Install OpenClaw: npm install -g openclaw@latest # Register the local server and set it as the default model: openclaw onboard --non-interactive --mode local \ --auth-choice custom-api-key \ --custom-base-url http://127.0.0.1:8080/v1 \ --custom-model-id "saik0s/comfy_backup:Q8_0" \ --custom-provider-id llama-cpp \ --custom-compatibility openai \ --custom-text-input \ --accept-risk \ --skip-health
Run OpenClaw
openclaw agent --local --agent main --message "Hello from Hugging Face"
- Docker Model Runner
How to use saik0s/comfy_backup with Docker Model Runner:
docker model run hf.co/saik0s/comfy_backup:Q8_0
- Lemonade
How to use saik0s/comfy_backup with Lemonade:
Pull the model
# Download Lemonade from https://lemonade-server.ai/ lemonade pull saik0s/comfy_backup:Q8_0
Run and chat with the model
lemonade run user.comfy_backup-Q8_0
List all available models
lemonade list
Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- custom_nodes/10S-Comfy-nodes/workflows/10Eros_10SNodes_I2V_v3_TiledSampler.json +1 -0
- custom_nodes/10S-Comfy-nodes/workflows/10Eros_10SNodes_LikenessGuideHelper_I2V_v3.2.json +0 -0
- custom_nodes/ComfyUI-GGUF/tools/README.md +93 -0
- custom_nodes/ComfyUI-GGUF/tools/convert.py +365 -0
- custom_nodes/ComfyUI-GGUF/tools/fix_5d_tensors.py +82 -0
- custom_nodes/ComfyUI-GGUF/tools/fix_lines_ending.py +31 -0
- custom_nodes/ComfyUI-GGUF/tools/lcpp.patch +451 -0
- custom_nodes/ComfyUI-GGUF/tools/read_tensors.py +21 -0
- custom_nodes/ComfyUI-HuggingFace/.github/workflows/publish.yml +28 -0
- custom_nodes/ComfyUI-HuggingFace/api/__init__.py +0 -0
- custom_nodes/ComfyUI-HuggingFace/api/huggingface.py +219 -0
- custom_nodes/ComfyUI-HuggingFace/downloader/__init__.py +0 -0
- custom_nodes/ComfyUI-HuggingFace/downloader/chunk_downloader.py +574 -0
- custom_nodes/ComfyUI-HuggingFace/downloader/manager.py +996 -0
- custom_nodes/ComfyUI-HuggingFace/server/__init__.py +5 -0
- custom_nodes/ComfyUI-HuggingFace/server/routes/CancelDownload.py +44 -0
- custom_nodes/ComfyUI-HuggingFace/server/routes/ClearHistory.py +32 -0
- custom_nodes/ComfyUI-HuggingFace/server/routes/DownloadModel.py +186 -0
- custom_nodes/ComfyUI-HuggingFace/server/routes/GetBaseModels.py +51 -0
- custom_nodes/ComfyUI-HuggingFace/server/routes/GetModelDetails.py +120 -0
- custom_nodes/ComfyUI-HuggingFace/server/routes/GetModelDirs.py +337 -0
- custom_nodes/ComfyUI-HuggingFace/server/routes/GetModelTypes.py +31 -0
- custom_nodes/ComfyUI-HuggingFace/server/routes/GetStatus.py +19 -0
- custom_nodes/ComfyUI-HuggingFace/server/routes/OpenPath.py +58 -0
- custom_nodes/ComfyUI-HuggingFace/server/routes/RetryDownload.py +39 -0
- custom_nodes/ComfyUI-HuggingFace/server/routes/SearchModels.py +121 -0
- custom_nodes/ComfyUI-HuggingFace/server/routes/__init__.py +21 -0
- custom_nodes/ComfyUI-HuggingFace/server/utils.py +187 -0
- custom_nodes/ComfyUI-HuggingFace/utils/__init__.py +15 -0
- custom_nodes/ComfyUI-HuggingFace/utils/helpers.py +224 -0
- custom_nodes/ComfyUI-HuggingFace/web/js/api/huggingface.js +164 -0
- custom_nodes/ComfyUI-HuggingFace/web/js/huggingfaceDownloader.css +753 -0
- custom_nodes/ComfyUI-HuggingFace/web/js/huggingfaceDownloader.js +89 -0
- custom_nodes/ComfyUI-HuggingFace/web/js/ui/UI.js +465 -0
- custom_nodes/ComfyUI-HuggingFace/web/js/ui/feedback.js +41 -0
- custom_nodes/ComfyUI-HuggingFace/web/js/ui/handlers/downloadHandler.js +106 -0
- custom_nodes/ComfyUI-HuggingFace/web/js/ui/handlers/eventListeners.js +248 -0
- custom_nodes/ComfyUI-HuggingFace/web/js/ui/handlers/searchHandler.js +39 -0
- custom_nodes/ComfyUI-HuggingFace/web/js/ui/handlers/settingsHandler.js +160 -0
- custom_nodes/ComfyUI-HuggingFace/web/js/ui/handlers/statusHandler.js +154 -0
- custom_nodes/ComfyUI-HuggingFace/web/js/ui/previewRenderer.js +96 -0
- custom_nodes/ComfyUI-HuggingFace/web/js/ui/searchRenderer.js +196 -0
- custom_nodes/ComfyUI-HuggingFace/web/js/ui/statusRenderer.js +124 -0
- custom_nodes/ComfyUI-HuggingFace/web/js/ui/templates.js +193 -0
- custom_nodes/ComfyUI-HuggingFace/web/js/utils/cookies.js +24 -0
- custom_nodes/ComfyUI-HuggingFace/web/js/utils/dom.js +32 -0
- custom_nodes/ComfyUI-KJNodes/.github/workflows/publish.yml +25 -0
- custom_nodes/ComfyUI-KJNodes/docs/images/319121566-05f66385-7568-4b1f-8bbc-11053660b02f.png +0 -0
- custom_nodes/ComfyUI-KJNodes/docs/images/319121636-706b5081-9120-4a29-bd76-901691ada688.png +0 -0
- 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('« 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 »', 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,'<');
|
| 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">×</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><global_root>/<model_type></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 |
+
}
|