Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0960817698 |
@@ -1913,7 +1913,7 @@
|
||||
"The painted area, grown a little, is filled with flat gray, so Krea 2 knows where to paint. The plain crop goes along too."
|
||||
],
|
||||
"widgets_values_named": {
|
||||
"text": "The painted area, grown a little, is filled with flat gray. Krea 2 never sees what you are replacing."
|
||||
"text": "The painted area, grown a little, is filled with flat gray, so Krea 2 knows where to paint. The plain crop goes along too."
|
||||
},
|
||||
"color": "#1f2c38",
|
||||
"bgcolor": "#2a3b4a"
|
||||
@@ -3052,7 +3052,7 @@
|
||||
"The gray-filled crop is the reference. Painting starts from the plain crop, inside the noise mask: the painted area, widened by 24 px."
|
||||
],
|
||||
"widgets_values_named": {
|
||||
"text": "The gray-filled crop is the reference. The noise mask is the grown painted area, widened by 24 px. Krea 2 paints only inside it."
|
||||
"text": "The gray-filled crop is the reference. Painting starts from the plain crop, inside the noise mask: the painted area, widened by 24 px."
|
||||
},
|
||||
"color": "#1f2c38",
|
||||
"bgcolor": "#2a3b4a"
|
||||
|
||||
@@ -2139,10 +2139,10 @@
|
||||
"title": "Note",
|
||||
"properties": {},
|
||||
"widgets_values": [
|
||||
"Blend in fades the model's picture into yours just inside its edge, with no tone shift. The new area stays as painted."
|
||||
"Blend in fades the model's picture into yours just inside its edge. Tone match is fully on, so the new area is adjusted to your picture's tone."
|
||||
],
|
||||
"widgets_values_named": {
|
||||
"text": "Blend in fades the model's picture into yours just inside its edge, with no tone shift. The new area stays as painted."
|
||||
"text": "Blend in fades the model's picture into yours just inside its edge. Tone match is fully on, so the new area is adjusted to your picture's tone."
|
||||
},
|
||||
"color": "#1f2c38",
|
||||
"bgcolor": "#2a3b4a"
|
||||
|
||||
@@ -37,10 +37,10 @@
|
||||
"Node name for S&R": "AUSBOSS_NODES_WorkflowNote"
|
||||
},
|
||||
"widgets_values": [
|
||||
"{\"v\": 1, \"title\": \"Qwen Image 2.1 Inpaint\", \"subtitle\": \"Click a thing in your picture and say what to change. Only that part is repainted.\", \"author\": \"AusBoss\", \"layout\": \"card\", \"accent\": \"\", \"body\": \"1. Load your image. You can crop it right there with the blue handles.\\n2. On **Mask by Touch**, click the thing you want to change. It lights up before you click.\\n3. In **What to change**, say it the way you would ask a person: \\\"Replace the cup with a glass of orange juice.\\\"\\n4. Press **Run**.\\n\\nOnly the picked part is repainted. Everything outside it stays your own picture.\\n\\n### Tips\\n- Wrong thing picked? The arrows under the picture swap it for the next thing it could mean.\\n- Name everything that should go: \\\"the iced coffee and its straw\\\". What you do not name can stay.\\n- To remove something, pick it and write \\\"Remove the cup.\\\" Pick its shadow too, or the shadow stays.\\n- To add something, switch to **Paint** and paint where it should go, with some room. Say how big: \\\"Add small black sunglasses on her eyes.\\\"\\n- Not what you wanted? Run it again. The seed changes each run.\\n\\n### Switches\\n- **Fast** on: 8 steps with the Viggle Turbo LoRA from the list below. Off: 25 steps without it, about twice as long.\\n\\nNeeds ComfyUI 0.38 or newer and AusBoss nodes 2.12.0 or newer (update first).\", \"models\": [{\"name\": \"qwen_image_2.1_int8_convrot.safetensors\", \"dir\": \"diffusion_models\", \"size\": \"7.26 GB\", \"url\": \"https://huggingface.co/Comfy-Org/Qwen-Image-2.1/resolve/main/diffusion_models/qwen_image_2.1_int8_convrot.safetensors\", \"note\": \"\"}, {\"name\": \"qwen3vl_8b_int8_convrot.safetensors\", \"dir\": \"text_encoders\", \"size\": \"9.35 GB\", \"url\": \"https://huggingface.co/Comfy-Org/Qwen-Image-2.1/resolve/main/text_encoders/qwen3vl_8b_int8_convrot.safetensors\", \"note\": \"\"}, {\"name\": \"qwen_image_2.1_vae_bf16.safetensors\", \"dir\": \"vae\", \"size\": \"676 MB\", \"url\": \"https://huggingface.co/Comfy-Org/Qwen-Image-2.1/resolve/main/vae/qwen_image_2.1_vae_bf16.safetensors\", \"note\": \"\"}, {\"name\": \"sam3.1_multiplex_fp16.safetensors\", \"dir\": \"checkpoints\", \"size\": \"1.75 GB\", \"url\": \"https://huggingface.co/Comfy-Org/sam3.1/resolve/main/checkpoints/sam3.1_multiplex_fp16.safetensors\", \"note\": \"by Meta, packaged by Comfy-Org\"}, {\"name\": \"Qwen-Image-2.1-viggle-turbo-v0.3-6step-lora-r128.safetensors\", \"dir\": \"loras\", \"size\": \"680 MB\", \"url\": \"https://huggingface.co/Viggle/Qwen-Image-2.1-viggle-turbo/resolve/main/Qwen-Image-2.1-viggle-turbo-v0.3-6step-lora-r128.safetensors\", \"note\": \"optional, for Fast · Qwen Research License, research use\"}], \"packs\": [{\"name\": \"ComfyUI-AusBoss\", \"url\": \"https://github.com/ausboss/ComfyUI-AusBoss\", \"node\": \"AUSBOSS_NODES_MaskByTouch\"}], \"links\": [{\"label\": \"YouTube · this workflow\", \"url\": \"https://youtu.be/iMgaiXoB0v4\"}, {\"label\": \"GitHub · ausboss\", \"url\": \"https://github.com/ausboss\"}, {\"label\": \"Civitai · AusBoss\", \"url\": \"https://civitai.com/user/AusBoss\"}, {\"label\": \"X · Zanzibased\", \"url\": \"https://x.com/Zanzibased\"}]}"
|
||||
"{\"v\": 1, \"title\": \"Qwen Image 2.1 Inpaint\", \"subtitle\": \"Click a thing in your picture and say what to change. Only that part is repainted.\", \"author\": \"AusBoss\", \"layout\": \"card\", \"accent\": \"\", \"body\": \"1. Load your image. You can crop it right there with the blue handles.\\n2. On **Mask by Touch**, click the thing you want to change. It lights up before you click.\\n3. In **What to change**, say it the way you would ask a person: \\\"Replace the cup with a glass of orange juice.\\\"\\n4. Press **Run**.\\n\\nOnly the picked part is repainted. Everything outside it stays your own picture.\\n\\n### Tips\\n- Wrong thing picked? The arrows under the picture swap it for the next thing it could mean.\\n- Name everything that should go: \\\"the iced coffee and its straw\\\". What you do not name can stay.\\n- To remove something, pick it and write \\\"Remove the cup.\\\" Pick its shadow too, or the shadow stays.\\n- To add something, switch to **Paint** and paint where it should go, with some room. Say how big: \\\"Add small black sunglasses on her eyes.\\\"\\n- Not what you wanted? Change **Seed** and run it again. It stays fixed between runs.\\n\\n### Switches\\n- **Fast** on: 8 steps with the Viggle Turbo LoRA from the list below. Off: 25 steps without it, about twice as long.\\n\\nNeeds ComfyUI 0.38 or newer and AusBoss nodes 2.12.0 or newer (update first).\", \"models\": [{\"name\": \"qwen_image_2.1_int8_convrot.safetensors\", \"dir\": \"diffusion_models\", \"size\": \"7.26 GB\", \"url\": \"https://huggingface.co/Comfy-Org/Qwen-Image-2.1/resolve/main/diffusion_models/qwen_image_2.1_int8_convrot.safetensors\", \"note\": \"\"}, {\"name\": \"qwen3vl_8b_int8_convrot.safetensors\", \"dir\": \"text_encoders\", \"size\": \"9.35 GB\", \"url\": \"https://huggingface.co/Comfy-Org/Qwen-Image-2.1/resolve/main/text_encoders/qwen3vl_8b_int8_convrot.safetensors\", \"note\": \"\"}, {\"name\": \"qwen_image_2.1_vae_bf16.safetensors\", \"dir\": \"vae\", \"size\": \"676 MB\", \"url\": \"https://huggingface.co/Comfy-Org/Qwen-Image-2.1/resolve/main/vae/qwen_image_2.1_vae_bf16.safetensors\", \"note\": \"\"}, {\"name\": \"sam3.1_multiplex_fp16.safetensors\", \"dir\": \"checkpoints\", \"size\": \"1.75 GB\", \"url\": \"https://huggingface.co/Comfy-Org/sam3.1/resolve/main/checkpoints/sam3.1_multiplex_fp16.safetensors\", \"note\": \"by Meta, packaged by Comfy-Org\"}, {\"name\": \"Qwen-Image-2.1-viggle-turbo-v0.3-6step-lora-r128.safetensors\", \"dir\": \"loras\", \"size\": \"680 MB\", \"url\": \"https://huggingface.co/Viggle/Qwen-Image-2.1-viggle-turbo/resolve/main/Qwen-Image-2.1-viggle-turbo-v0.3-6step-lora-r128.safetensors\", \"note\": \"optional, for Fast · Qwen Research License, research use\"}], \"packs\": [{\"name\": \"ComfyUI-AusBoss\", \"url\": \"https://github.com/ausboss/ComfyUI-AusBoss\", \"node\": \"AUSBOSS_NODES_MaskByTouch\"}], \"links\": [{\"label\": \"YouTube · this workflow\", \"url\": \"https://youtu.be/iMgaiXoB0v4\"}, {\"label\": \"GitHub · ausboss\", \"url\": \"https://github.com/ausboss\"}, {\"label\": \"Civitai · AusBoss\", \"url\": \"https://civitai.com/user/AusBoss\"}, {\"label\": \"X · Zanzibased\", \"url\": \"https://x.com/Zanzibased\"}]}"
|
||||
],
|
||||
"widgets_values_named": {
|
||||
"note": "{\"v\": 1, \"title\": \"Qwen Image 2.1 Inpaint\", \"subtitle\": \"Click a thing in your picture and say what to change. Only that part is repainted.\", \"author\": \"AusBoss\", \"layout\": \"card\", \"accent\": \"\", \"body\": \"1. Load your image. You can crop it right there with the blue handles.\\n2. On **Mask by Touch**, click the thing you want to change. It lights up before you click.\\n3. In **What to change**, say it the way you would ask a person: \\\"Replace the cup with a glass of orange juice.\\\"\\n4. Press **Run**.\\n\\nOnly the picked part is repainted. Everything outside it stays your own picture.\\n\\n### Tips\\n- Wrong thing picked? The arrows under the picture swap it for the next thing it could mean.\\n- Name everything that should go: \\\"the iced coffee and its straw\\\". What you do not name can stay.\\n- To remove something, pick it and write \\\"Remove the cup.\\\" Pick its shadow too, or the shadow stays.\\n- To add something, switch to **Paint** and paint where it should go, with some room. Say how big: \\\"Add small black sunglasses on her eyes.\\\"\\n- Not what you wanted? Run it again. The seed changes each run.\\n\\n### Switches\\n- **Fast** on: 8 steps with the Viggle Turbo LoRA from the list below. Off: 25 steps without it, about twice as long.\\n\\nNeeds ComfyUI 0.38 or newer and AusBoss nodes 2.12.0 or newer (update first).\", \"models\": [{\"name\": \"qwen_image_2.1_int8_convrot.safetensors\", \"dir\": \"diffusion_models\", \"size\": \"7.26 GB\", \"url\": \"https://huggingface.co/Comfy-Org/Qwen-Image-2.1/resolve/main/diffusion_models/qwen_image_2.1_int8_convrot.safetensors\", \"note\": \"\"}, {\"name\": \"qwen3vl_8b_int8_convrot.safetensors\", \"dir\": \"text_encoders\", \"size\": \"9.35 GB\", \"url\": \"https://huggingface.co/Comfy-Org/Qwen-Image-2.1/resolve/main/text_encoders/qwen3vl_8b_int8_convrot.safetensors\", \"note\": \"\"}, {\"name\": \"qwen_image_2.1_vae_bf16.safetensors\", \"dir\": \"vae\", \"size\": \"676 MB\", \"url\": \"https://huggingface.co/Comfy-Org/Qwen-Image-2.1/resolve/main/vae/qwen_image_2.1_vae_bf16.safetensors\", \"note\": \"\"}, {\"name\": \"sam3.1_multiplex_fp16.safetensors\", \"dir\": \"checkpoints\", \"size\": \"1.75 GB\", \"url\": \"https://huggingface.co/Comfy-Org/sam3.1/resolve/main/checkpoints/sam3.1_multiplex_fp16.safetensors\", \"note\": \"by Meta, packaged by Comfy-Org\"}, {\"name\": \"Qwen-Image-2.1-viggle-turbo-v0.3-6step-lora-r128.safetensors\", \"dir\": \"loras\", \"size\": \"680 MB\", \"url\": \"https://huggingface.co/Viggle/Qwen-Image-2.1-viggle-turbo/resolve/main/Qwen-Image-2.1-viggle-turbo-v0.3-6step-lora-r128.safetensors\", \"note\": \"optional, for Fast · Qwen Research License, research use\"}], \"packs\": [{\"name\": \"ComfyUI-AusBoss\", \"url\": \"https://github.com/ausboss/ComfyUI-AusBoss\", \"node\": \"AUSBOSS_NODES_MaskByTouch\"}], \"links\": [{\"label\": \"YouTube · this workflow\", \"url\": \"https://youtu.be/iMgaiXoB0v4\"}, {\"label\": \"GitHub · ausboss\", \"url\": \"https://github.com/ausboss\"}, {\"label\": \"Civitai · AusBoss\", \"url\": \"https://civitai.com/user/AusBoss\"}, {\"label\": \"X · Zanzibased\", \"url\": \"https://x.com/Zanzibased\"}]}"
|
||||
"note": "{\"v\": 1, \"title\": \"Qwen Image 2.1 Inpaint\", \"subtitle\": \"Click a thing in your picture and say what to change. Only that part is repainted.\", \"author\": \"AusBoss\", \"layout\": \"card\", \"accent\": \"\", \"body\": \"1. Load your image. You can crop it right there with the blue handles.\\n2. On **Mask by Touch**, click the thing you want to change. It lights up before you click.\\n3. In **What to change**, say it the way you would ask a person: \\\"Replace the cup with a glass of orange juice.\\\"\\n4. Press **Run**.\\n\\nOnly the picked part is repainted. Everything outside it stays your own picture.\\n\\n### Tips\\n- Wrong thing picked? The arrows under the picture swap it for the next thing it could mean.\\n- Name everything that should go: \\\"the iced coffee and its straw\\\". What you do not name can stay.\\n- To remove something, pick it and write \\\"Remove the cup.\\\" Pick its shadow too, or the shadow stays.\\n- To add something, switch to **Paint** and paint where it should go, with some room. Say how big: \\\"Add small black sunglasses on her eyes.\\\"\\n- Not what you wanted? Change **Seed** and run it again. It stays fixed between runs.\\n\\n### Switches\\n- **Fast** on: 8 steps with the Viggle Turbo LoRA from the list below. Off: 25 steps without it, about twice as long.\\n\\nNeeds ComfyUI 0.38 or newer and AusBoss nodes 2.12.0 or newer (update first).\", \"models\": [{\"name\": \"qwen_image_2.1_int8_convrot.safetensors\", \"dir\": \"diffusion_models\", \"size\": \"7.26 GB\", \"url\": \"https://huggingface.co/Comfy-Org/Qwen-Image-2.1/resolve/main/diffusion_models/qwen_image_2.1_int8_convrot.safetensors\", \"note\": \"\"}, {\"name\": \"qwen3vl_8b_int8_convrot.safetensors\", \"dir\": \"text_encoders\", \"size\": \"9.35 GB\", \"url\": \"https://huggingface.co/Comfy-Org/Qwen-Image-2.1/resolve/main/text_encoders/qwen3vl_8b_int8_convrot.safetensors\", \"note\": \"\"}, {\"name\": \"qwen_image_2.1_vae_bf16.safetensors\", \"dir\": \"vae\", \"size\": \"676 MB\", \"url\": \"https://huggingface.co/Comfy-Org/Qwen-Image-2.1/resolve/main/vae/qwen_image_2.1_vae_bf16.safetensors\", \"note\": \"\"}, {\"name\": \"sam3.1_multiplex_fp16.safetensors\", \"dir\": \"checkpoints\", \"size\": \"1.75 GB\", \"url\": \"https://huggingface.co/Comfy-Org/sam3.1/resolve/main/checkpoints/sam3.1_multiplex_fp16.safetensors\", \"note\": \"by Meta, packaged by Comfy-Org\"}, {\"name\": \"Qwen-Image-2.1-viggle-turbo-v0.3-6step-lora-r128.safetensors\", \"dir\": \"loras\", \"size\": \"680 MB\", \"url\": \"https://huggingface.co/Viggle/Qwen-Image-2.1-viggle-turbo/resolve/main/Qwen-Image-2.1-viggle-turbo-v0.3-6step-lora-r128.safetensors\", \"note\": \"optional, for Fast · Qwen Research License, research use\"}], \"packs\": [{\"name\": \"ComfyUI-AusBoss\", \"url\": \"https://github.com/ausboss/ComfyUI-AusBoss\", \"node\": \"AUSBOSS_NODES_MaskByTouch\"}], \"links\": [{\"label\": \"YouTube · this workflow\", \"url\": \"https://youtu.be/iMgaiXoB0v4\"}, {\"label\": \"GitHub · ausboss\", \"url\": \"https://github.com/ausboss\"}, {\"label\": \"Civitai · AusBoss\", \"url\": \"https://civitai.com/user/AusBoss\"}, {\"label\": \"X · Zanzibased\", \"url\": \"https://x.com/Zanzibased\"}]}"
|
||||
},
|
||||
"color": "#14424d",
|
||||
"bgcolor": "#161f21"
|
||||
|
||||
@@ -37,10 +37,10 @@
|
||||
"ver": "2.9.0"
|
||||
},
|
||||
"widgets_values": [
|
||||
"{\"v\": 1, \"title\": \"Qwen Image 2.1 Upscale + Restore\", \"subtitle\": \"Load a small, soft or worn picture. Qwen Image 2.1 redraws it sharp at the megapixels you set, in its own colors.\", \"author\": \"AusBoss\", \"layout\": \"card\", \"accent\": \"\", \"body\": \"[▶ AusBoss on YouTube](https://www.youtube.com/@ausbossdev) · videos on these nodes and workflows\\n\\n---\\n\\n1. Load your image. You can crop it right there with the blue handles.\\n2. Set **Megapixels**: the size of the result.\\n3. Press **Run**.\\n4. Slide **Before and after** to compare. The result is saved as a PNG.\\n\\n### What you get\\n\\n- Your picture redrawn sharp, at the size you asked for.\\n- Its own colors, also on a noisy or faded photo.\\n\\n### Megapixels\\n\\n- Up to about 2: one pass.\\n- Over that: redrawn in tiles of about 2 megapixels and joined. 4 takes two tiles, 8 takes four.\\n- A small picture is redrawn at up to 8 times its own megapixels, then enlarged the plain way.\\n- A picture of 1 megapixel or more only looks better when it grows about 1.6 times per side.\\n\\n### Switches\\n\\n- **Fast** on: 8 steps with the Viggle Turbo LoRA. Off: 25 steps without it, about three times as long.\\n\\n### Tips\\n\\n- It is a redraw. Fine detail is made up to fit, so a face stays the same person, not the same pixels.\\n- Small lettering on signs and labels can come back misspelled.\\n- Each tile is matched to your picture's colors. To let Qwen's own colors through, lower **Keep colors** inside the box.\\n- If the run stops at a red **LoRA Loader**, a LoRA that is switched on is missing. Download it from the list below.\\n- Not tested on blur from a moving camera or missed focus.\\n\\nNeeds ComfyUI 0.38 or newer and AusBoss nodes 2.9.0 or newer (update first).\", \"models\": [{\"name\": \"qwen_image_2.1_int8_convrot.safetensors\", \"dir\": \"diffusion_models\", \"size\": \"7.26 GB\", \"url\": \"https://huggingface.co/Comfy-Org/Qwen-Image-2.1/resolve/main/diffusion_models/qwen_image_2.1_int8_convrot.safetensors\", \"note\": \"\"}, {\"name\": \"qwen3vl_8b_int8_convrot.safetensors\", \"dir\": \"text_encoders\", \"size\": \"9.35 GB\", \"url\": \"https://huggingface.co/Comfy-Org/Qwen-Image-2.1/resolve/main/text_encoders/qwen3vl_8b_int8_convrot.safetensors\", \"note\": \"\"}, {\"name\": \"qwen_image_2.1_vae_bf16.safetensors\", \"dir\": \"vae\", \"size\": \"676 MB\", \"url\": \"https://huggingface.co/Comfy-Org/Qwen-Image-2.1/resolve/main/vae/qwen_image_2.1_vae_bf16.safetensors\", \"note\": \"\"}, {\"name\": \"qwen-image-2.1-restore.safetensors\", \"dir\": \"loras\", \"size\": \"159 MB\", \"url\": \"https://huggingface.co/ausboss/Qwen-Image-2.1-Restore-LoRA/resolve/main/qwen-image-2.1-restore.safetensors\", \"note\": \"keeps the result close to your picture\"}, {\"name\": \"Qwen-Image-2.1-viggle-turbo-v0.3-6step-lora-r128.safetensors\", \"dir\": \"loras\", \"size\": \"680 MB\", \"url\": \"https://huggingface.co/Viggle/Qwen-Image-2.1-viggle-turbo/resolve/main/Qwen-Image-2.1-viggle-turbo-v0.3-6step-lora-r128.safetensors\", \"note\": \"for Fast on · Qwen Research License, research use\"}], \"packs\": [{\"name\": \"ComfyUI-AusBoss\", \"url\": \"https://github.com/ausboss/ComfyUI-AusBoss\", \"node\": \"AUSBOSS_NODES_TiledUpscale\"}], \"links\": [{\"label\": \"YouTube · ausbossdev\", \"url\": \"https://www.youtube.com/@ausbossdev\"}, {\"label\": \"GitHub · ausboss\", \"url\": \"https://github.com/ausboss\"}, {\"label\": \"Civitai · AusBoss\", \"url\": \"https://civitai.com/user/AusBoss\"}, {\"label\": \"X · Zanzibased\", \"url\": \"https://x.com/Zanzibased\"}]}"
|
||||
"{\"v\": 1, \"title\": \"Qwen Image 2.1 Upscale + Restore\", \"subtitle\": \"Load a small, soft or worn picture. Qwen Image 2.1 redraws it sharp at the megapixels you set, in its own colors.\", \"author\": \"AusBoss\", \"layout\": \"card\", \"accent\": \"\", \"body\": \"[▶ AusBoss on YouTube](https://www.youtube.com/@ausbossdev) · videos on these nodes and workflows\\n\\n---\\n\\n1. Load your image. You can crop it right there with the blue handles.\\n2. Set **Megapixels**: the size of the result.\\n3. Press **Run**.\\n4. Slide **Before and after** to compare. The result is saved as a PNG.\\n\\n### What you get\\n\\n- Your picture redrawn sharp, at the size you asked for.\\n- Its own colors, also on a noisy or faded photo.\\n\\n### Megapixels\\n\\n- Up to about 2: one pass.\\n- Over that: redrawn in tiles of about 2 megapixels and joined. 4 takes two tiles, 8 takes four.\\n- The redraw limit for a small picture is 8 times its own megapixels or one roughly 2 megapixel tile, whichever is larger. The rest is enlarged the plain way.\\n- A picture of 1 megapixel or more only looks better when it grows about 1.6 times per side.\\n\\n### Switches\\n\\n- **Fast** on: 8 steps with the Viggle Turbo LoRA. Off: 25 steps without it, about three times as long.\\n\\n### Tips\\n\\n- It is a redraw. Fine detail is made up to fit, so a face stays the same person, not the same pixels.\\n- Small lettering on signs and labels can come back misspelled.\\n- Each tile is matched to your picture's colors. To let Qwen's own colors through, lower **Keep colors** inside the box.\\n- If the run stops at a red **LoRA Loader**, a LoRA that is switched on is missing. Download it from the list below.\\n- Not tested on blur from a moving camera or missed focus.\\n\\nNeeds ComfyUI 0.38 or newer and AusBoss nodes 2.9.0 or newer (update first).\", \"models\": [{\"name\": \"qwen_image_2.1_int8_convrot.safetensors\", \"dir\": \"diffusion_models\", \"size\": \"7.26 GB\", \"url\": \"https://huggingface.co/Comfy-Org/Qwen-Image-2.1/resolve/main/diffusion_models/qwen_image_2.1_int8_convrot.safetensors\", \"note\": \"\"}, {\"name\": \"qwen3vl_8b_int8_convrot.safetensors\", \"dir\": \"text_encoders\", \"size\": \"9.35 GB\", \"url\": \"https://huggingface.co/Comfy-Org/Qwen-Image-2.1/resolve/main/text_encoders/qwen3vl_8b_int8_convrot.safetensors\", \"note\": \"\"}, {\"name\": \"qwen_image_2.1_vae_bf16.safetensors\", \"dir\": \"vae\", \"size\": \"676 MB\", \"url\": \"https://huggingface.co/Comfy-Org/Qwen-Image-2.1/resolve/main/vae/qwen_image_2.1_vae_bf16.safetensors\", \"note\": \"\"}, {\"name\": \"qwen-image-2.1-restore.safetensors\", \"dir\": \"loras\", \"size\": \"159 MB\", \"url\": \"https://huggingface.co/ausboss/Qwen-Image-2.1-Restore-LoRA/resolve/main/qwen-image-2.1-restore.safetensors\", \"note\": \"keeps the result close to your picture\"}, {\"name\": \"Qwen-Image-2.1-viggle-turbo-v0.3-6step-lora-r128.safetensors\", \"dir\": \"loras\", \"size\": \"680 MB\", \"url\": \"https://huggingface.co/Viggle/Qwen-Image-2.1-viggle-turbo/resolve/main/Qwen-Image-2.1-viggle-turbo-v0.3-6step-lora-r128.safetensors\", \"note\": \"for Fast on · Qwen Research License, research use\"}], \"packs\": [{\"name\": \"ComfyUI-AusBoss\", \"url\": \"https://github.com/ausboss/ComfyUI-AusBoss\", \"node\": \"AUSBOSS_NODES_TiledUpscale\"}], \"links\": [{\"label\": \"YouTube · ausbossdev\", \"url\": \"https://www.youtube.com/@ausbossdev\"}, {\"label\": \"GitHub · ausboss\", \"url\": \"https://github.com/ausboss\"}, {\"label\": \"Civitai · AusBoss\", \"url\": \"https://civitai.com/user/AusBoss\"}, {\"label\": \"X · Zanzibased\", \"url\": \"https://x.com/Zanzibased\"}]}"
|
||||
],
|
||||
"widgets_values_named": {
|
||||
"note": "{\"v\": 1, \"title\": \"Qwen Image 2.1 Upscale + Restore\", \"subtitle\": \"Load a small, soft or worn picture. Qwen Image 2.1 redraws it sharp at the megapixels you set, in its own colors.\", \"author\": \"AusBoss\", \"layout\": \"card\", \"accent\": \"\", \"body\": \"[▶ AusBoss on YouTube](https://www.youtube.com/@ausbossdev) · videos on these nodes and workflows\\n\\n---\\n\\n1. Load your image. You can crop it right there with the blue handles.\\n2. Set **Megapixels**: the size of the result.\\n3. Press **Run**.\\n4. Slide **Before and after** to compare. The result is saved as a PNG.\\n\\n### What you get\\n\\n- Your picture redrawn sharp, at the size you asked for.\\n- Its own colors, also on a noisy or faded photo.\\n\\n### Megapixels\\n\\n- Up to about 2: one pass.\\n- Over that: redrawn in tiles of about 2 megapixels and joined. 4 takes two tiles, 8 takes four.\\n- A small picture is redrawn at up to 8 times its own megapixels, then enlarged the plain way.\\n- A picture of 1 megapixel or more only looks better when it grows about 1.6 times per side.\\n\\n### Switches\\n\\n- **Fast** on: 8 steps with the Viggle Turbo LoRA. Off: 25 steps without it, about three times as long.\\n\\n### Tips\\n\\n- It is a redraw. Fine detail is made up to fit, so a face stays the same person, not the same pixels.\\n- Small lettering on signs and labels can come back misspelled.\\n- Each tile is matched to your picture's colors. To let Qwen's own colors through, lower **Keep colors** inside the box.\\n- If the run stops at a red **LoRA Loader**, a LoRA that is switched on is missing. Download it from the list below.\\n- Not tested on blur from a moving camera or missed focus.\\n\\nNeeds ComfyUI 0.38 or newer and AusBoss nodes 2.9.0 or newer (update first).\", \"models\": [{\"name\": \"qwen_image_2.1_int8_convrot.safetensors\", \"dir\": \"diffusion_models\", \"size\": \"7.26 GB\", \"url\": \"https://huggingface.co/Comfy-Org/Qwen-Image-2.1/resolve/main/diffusion_models/qwen_image_2.1_int8_convrot.safetensors\", \"note\": \"\"}, {\"name\": \"qwen3vl_8b_int8_convrot.safetensors\", \"dir\": \"text_encoders\", \"size\": \"9.35 GB\", \"url\": \"https://huggingface.co/Comfy-Org/Qwen-Image-2.1/resolve/main/text_encoders/qwen3vl_8b_int8_convrot.safetensors\", \"note\": \"\"}, {\"name\": \"qwen_image_2.1_vae_bf16.safetensors\", \"dir\": \"vae\", \"size\": \"676 MB\", \"url\": \"https://huggingface.co/Comfy-Org/Qwen-Image-2.1/resolve/main/vae/qwen_image_2.1_vae_bf16.safetensors\", \"note\": \"\"}, {\"name\": \"qwen-image-2.1-restore.safetensors\", \"dir\": \"loras\", \"size\": \"159 MB\", \"url\": \"https://huggingface.co/ausboss/Qwen-Image-2.1-Restore-LoRA/resolve/main/qwen-image-2.1-restore.safetensors\", \"note\": \"keeps the result close to your picture\"}, {\"name\": \"Qwen-Image-2.1-viggle-turbo-v0.3-6step-lora-r128.safetensors\", \"dir\": \"loras\", \"size\": \"680 MB\", \"url\": \"https://huggingface.co/Viggle/Qwen-Image-2.1-viggle-turbo/resolve/main/Qwen-Image-2.1-viggle-turbo-v0.3-6step-lora-r128.safetensors\", \"note\": \"for Fast on · Qwen Research License, research use\"}], \"packs\": [{\"name\": \"ComfyUI-AusBoss\", \"url\": \"https://github.com/ausboss/ComfyUI-AusBoss\", \"node\": \"AUSBOSS_NODES_TiledUpscale\"}], \"links\": [{\"label\": \"YouTube · ausbossdev\", \"url\": \"https://www.youtube.com/@ausbossdev\"}, {\"label\": \"GitHub · ausboss\", \"url\": \"https://github.com/ausboss\"}, {\"label\": \"Civitai · AusBoss\", \"url\": \"https://civitai.com/user/AusBoss\"}, {\"label\": \"X · Zanzibased\", \"url\": \"https://x.com/Zanzibased\"}]}"
|
||||
"note": "{\"v\": 1, \"title\": \"Qwen Image 2.1 Upscale + Restore\", \"subtitle\": \"Load a small, soft or worn picture. Qwen Image 2.1 redraws it sharp at the megapixels you set, in its own colors.\", \"author\": \"AusBoss\", \"layout\": \"card\", \"accent\": \"\", \"body\": \"[▶ AusBoss on YouTube](https://www.youtube.com/@ausbossdev) · videos on these nodes and workflows\\n\\n---\\n\\n1. Load your image. You can crop it right there with the blue handles.\\n2. Set **Megapixels**: the size of the result.\\n3. Press **Run**.\\n4. Slide **Before and after** to compare. The result is saved as a PNG.\\n\\n### What you get\\n\\n- Your picture redrawn sharp, at the size you asked for.\\n- Its own colors, also on a noisy or faded photo.\\n\\n### Megapixels\\n\\n- Up to about 2: one pass.\\n- Over that: redrawn in tiles of about 2 megapixels and joined. 4 takes two tiles, 8 takes four.\\n- The redraw limit for a small picture is 8 times its own megapixels or one roughly 2 megapixel tile, whichever is larger. The rest is enlarged the plain way.\\n- A picture of 1 megapixel or more only looks better when it grows about 1.6 times per side.\\n\\n### Switches\\n\\n- **Fast** on: 8 steps with the Viggle Turbo LoRA. Off: 25 steps without it, about three times as long.\\n\\n### Tips\\n\\n- It is a redraw. Fine detail is made up to fit, so a face stays the same person, not the same pixels.\\n- Small lettering on signs and labels can come back misspelled.\\n- Each tile is matched to your picture's colors. To let Qwen's own colors through, lower **Keep colors** inside the box.\\n- If the run stops at a red **LoRA Loader**, a LoRA that is switched on is missing. Download it from the list below.\\n- Not tested on blur from a moving camera or missed focus.\\n\\nNeeds ComfyUI 0.38 or newer and AusBoss nodes 2.9.0 or newer (update first).\", \"models\": [{\"name\": \"qwen_image_2.1_int8_convrot.safetensors\", \"dir\": \"diffusion_models\", \"size\": \"7.26 GB\", \"url\": \"https://huggingface.co/Comfy-Org/Qwen-Image-2.1/resolve/main/diffusion_models/qwen_image_2.1_int8_convrot.safetensors\", \"note\": \"\"}, {\"name\": \"qwen3vl_8b_int8_convrot.safetensors\", \"dir\": \"text_encoders\", \"size\": \"9.35 GB\", \"url\": \"https://huggingface.co/Comfy-Org/Qwen-Image-2.1/resolve/main/text_encoders/qwen3vl_8b_int8_convrot.safetensors\", \"note\": \"\"}, {\"name\": \"qwen_image_2.1_vae_bf16.safetensors\", \"dir\": \"vae\", \"size\": \"676 MB\", \"url\": \"https://huggingface.co/Comfy-Org/Qwen-Image-2.1/resolve/main/vae/qwen_image_2.1_vae_bf16.safetensors\", \"note\": \"\"}, {\"name\": \"qwen-image-2.1-restore.safetensors\", \"dir\": \"loras\", \"size\": \"159 MB\", \"url\": \"https://huggingface.co/ausboss/Qwen-Image-2.1-Restore-LoRA/resolve/main/qwen-image-2.1-restore.safetensors\", \"note\": \"keeps the result close to your picture\"}, {\"name\": \"Qwen-Image-2.1-viggle-turbo-v0.3-6step-lora-r128.safetensors\", \"dir\": \"loras\", \"size\": \"680 MB\", \"url\": \"https://huggingface.co/Viggle/Qwen-Image-2.1-viggle-turbo/resolve/main/Qwen-Image-2.1-viggle-turbo-v0.3-6step-lora-r128.safetensors\", \"note\": \"for Fast on · Qwen Research License, research use\"}], \"packs\": [{\"name\": \"ComfyUI-AusBoss\", \"url\": \"https://github.com/ausboss/ComfyUI-AusBoss\", \"node\": \"AUSBOSS_NODES_TiledUpscale\"}], \"links\": [{\"label\": \"YouTube · ausbossdev\", \"url\": \"https://www.youtube.com/@ausbossdev\"}, {\"label\": \"GitHub · ausboss\", \"url\": \"https://github.com/ausboss\"}, {\"label\": \"Civitai · AusBoss\", \"url\": \"https://civitai.com/user/AusBoss\"}, {\"label\": \"X · Zanzibased\", \"url\": \"https://x.com/Zanzibased\"}]}"
|
||||
},
|
||||
"color": "#14424d",
|
||||
"bgcolor": "#161f21"
|
||||
|
||||
@@ -862,11 +862,8 @@ app.registerExtension({
|
||||
chainCallback(nodeType.prototype, "onExecuted", function (output) {
|
||||
if (isForeignRun()) return;
|
||||
const state = buildPanel(this, config);
|
||||
// Count-only results carry no replacement for a stale picture.
|
||||
if (state) {
|
||||
if (outputRecordQuery(output)) { state.stale = false; state.resultGone = false; }
|
||||
scheduleRefresh(state);
|
||||
}
|
||||
// A result arrived (Save Image sends one even with its preview off).
|
||||
if (state) { state.stale = false; state.resultGone = false; scheduleRefresh(state); }
|
||||
// Saved: the file's picture replaces the steps.
|
||||
if (state?.stepOverlay) {
|
||||
state.stepsSaved = stepRun().promptId;
|
||||
|
||||
@@ -170,7 +170,7 @@ api.addEventListener("execution_interrupted", () => settleAll());
|
||||
// second or two, so things light up while it is still looking.
|
||||
api.addEventListener(LOOK_EVENT, ({ detail }) => {
|
||||
try {
|
||||
if (!faces.size || !detail?.note || isForeignRun(detail.prompt_id)) return;
|
||||
if (!faces.size || !detail?.note) return;
|
||||
const id = String(detail.node ?? "");
|
||||
const root = app.rootGraph ?? app.graph;
|
||||
for (const face of faces) {
|
||||
@@ -183,39 +183,11 @@ api.addEventListener(LOOK_EVENT, ({ detail }) => {
|
||||
// A run that left the queue without ever reaching the node (it was deleted
|
||||
// there) would leave a face waiting for good. Once the queue is empty and a
|
||||
// face has waited a while, it stops waiting; the next touch asks again.
|
||||
let emptyQueueTimer = 0;
|
||||
let emptyQueueVersion = 0;
|
||||
function settleEmptyQueue() {
|
||||
const now = performance.now();
|
||||
let delay = Infinity;
|
||||
for (const face of faces) {
|
||||
if (!face.alive || !face.inFlight) continue;
|
||||
const left = 1501 - (now - face.askedAt);
|
||||
if (left <= 0) face.settle();
|
||||
else delay = Math.min(delay, left);
|
||||
}
|
||||
if (Number.isFinite(delay)) {
|
||||
const version = emptyQueueVersion;
|
||||
emptyQueueTimer = setTimeout(async () => {
|
||||
emptyQueueTimer = 0;
|
||||
try {
|
||||
const queue = await (await api.fetchApi("/queue")).json();
|
||||
if (version !== emptyQueueVersion) return;
|
||||
if (Array.isArray(queue?.queue_pending) && Array.isArray(queue?.queue_running)
|
||||
&& !queue.queue_pending.length && !queue.queue_running.length) settleEmptyQueue();
|
||||
} catch {
|
||||
// A failed queue read never cancels a request that may still run.
|
||||
}
|
||||
}, delay);
|
||||
}
|
||||
}
|
||||
api.addEventListener("status", ({ detail }) => {
|
||||
try {
|
||||
emptyQueueVersion += 1;
|
||||
clearTimeout(emptyQueueTimer);
|
||||
emptyQueueTimer = 0;
|
||||
if (!faces.size || detail?.exec_info?.queue_remaining !== 0) return;
|
||||
settleEmptyQueue();
|
||||
const now = performance.now();
|
||||
for (const face of faces) if (face.inFlight && now - face.askedAt > 1500) face.settle();
|
||||
} catch {
|
||||
// Never in the way of a run.
|
||||
}
|
||||
|
||||
@@ -1046,7 +1046,7 @@ export function pieceAt(planes, index, { sizes, inside, adding = true, ranks = n
|
||||
const size = sizes?.[piece] ?? 0;
|
||||
if (!size) continue;
|
||||
const picked = inside?.[piece] ?? 0;
|
||||
const slack = Math.min(size - 1, Math.max(4, size * NEARLY));
|
||||
const slack = Math.max(4, size * NEARLY);
|
||||
if (adding ? size - picked > slack : picked > slack) return piece;
|
||||
}
|
||||
return -1;
|
||||
|
||||
+15
-48
@@ -141,12 +141,11 @@ const TOUCH_REACH = 2;
|
||||
// The areas of picked things a picture keeps, as the node sent them.
|
||||
const AREAS_KEPT = 96;
|
||||
// Pictures the faces of a page have shown, newest last. A picture keeps its
|
||||
// pieces and the picked areas it has shown, so a tab switch, an undo or
|
||||
// another visit to the same picture costs nothing. Results belong to one face.
|
||||
// pieces and the picked areas it has shown, so a tab switch, an undo or a
|
||||
// second node on the same picture costs nothing.
|
||||
const PICTURES_KEPT = 3;
|
||||
const STATES_KEPT = 14;
|
||||
const pictures = new Map();
|
||||
let pictureSerial = 0;
|
||||
// A face asks for a run again on its own at most this often in a row.
|
||||
const RETRIES = 2;
|
||||
// The long side of the small copy the masks are worked on, when the face
|
||||
@@ -285,14 +284,13 @@ const widgetByName = (node, name) => node.widgets?.find((widget) => widget?.name
|
||||
|
||||
// ---- the pictures a page has shown ---------------------------------------
|
||||
|
||||
function pictureFor(note, face) {
|
||||
function pictureFor(note) {
|
||||
const width = Math.round(Number(note.width));
|
||||
const height = Math.round(Number(note.height));
|
||||
const scale = Number(note.scale) > 0 ? Number(note.scale) : 1;
|
||||
const cacheKey = face ? `${face.pictureId ??= ++pictureSerial}|${note.key}` : note.key;
|
||||
let entry = pictures.get(cacheKey);
|
||||
let entry = pictures.get(note.key);
|
||||
if (entry && (entry.width !== width || entry.height !== height)) entry = null;
|
||||
pictures.delete(cacheKey);
|
||||
pictures.delete(note.key);
|
||||
entry ??= {
|
||||
key: note.key,
|
||||
width,
|
||||
@@ -310,7 +308,7 @@ function pictureFor(note, face) {
|
||||
nameSeeds: new Map(),
|
||||
states: new Map(),
|
||||
};
|
||||
pictures.set(cacheKey, entry);
|
||||
pictures.set(note.key, entry);
|
||||
while (pictures.size > PICTURES_KEPT) pictures.delete(pictures.keys().next().value);
|
||||
return entry;
|
||||
}
|
||||
@@ -388,7 +386,7 @@ function validNote(note) {
|
||||
// whether the picture has points left to ask about.
|
||||
const piecesTag = (pieces) => `${pieces?.list?.length ?? 0}:${(Array.isArray(pieces?.planes) ? pieces.planes : []).map((file) => file?.filename).join(",")}`;
|
||||
|
||||
async function loadPieces(entry, pieces, generation) {
|
||||
async function loadPieces(entry, pieces) {
|
||||
const tag = piecesTag(pieces);
|
||||
const list = (Array.isArray(pieces.list) ? pieces.list : [])
|
||||
.filter((piece) => piece && Number.isFinite(Number(piece.x)) && Number.isFinite(Number(piece.y)))
|
||||
@@ -412,7 +410,6 @@ async function loadPieces(entry, pieces, generation) {
|
||||
});
|
||||
const files = (Array.isArray(pieces.planes) ? pieces.planes : []).filter(isFile);
|
||||
const images = await Promise.all(files.map((file) => loadImage(viewUrl(file, entry.key))));
|
||||
if (entry.generation !== generation) return;
|
||||
const planes = packPlanes(images.map((image) => readPixels(image, entry.pw, entry.ph)), entry.pw, entry.ph);
|
||||
const sizes = pieceSizes(planes, list.length);
|
||||
const overlaps = pieceOverlaps(planes, list.length);
|
||||
@@ -1404,7 +1401,7 @@ function syncTools(face) {
|
||||
// A click that waits for the look can be taken back like a pick.
|
||||
const waiting = live && face.pending.length > 0;
|
||||
face.undo.disabled = !live || (!some && !face.past.length && !waiting);
|
||||
face.clear.disabled = !some && !waiting && !String(face.touches?.value ?? "").trim();
|
||||
face.clear.disabled = !some && !waiting;
|
||||
if (face.lookButton) {
|
||||
const offer = live && face.snap && !face.held && !face.inFlight && !face.pic.scoped && Boolean(face.host.lookOnButton?.());
|
||||
face.lookButton.style.display = offer ? "" : "none";
|
||||
@@ -1483,28 +1480,6 @@ function ownLoader(node) {
|
||||
// for: a crop that is being dragged changes them many times a second.
|
||||
const OWN_SETTLE_MS = 700;
|
||||
|
||||
// One callback per source widget; removing a face removes its subscription.
|
||||
const sourceWatchers = new WeakMap();
|
||||
function sourceListeners(widget) {
|
||||
let listeners = sourceWatchers.get(widget);
|
||||
if (!listeners) {
|
||||
listeners = new Set();
|
||||
sourceWatchers.set(widget, listeners);
|
||||
chainCallback(widget, "callback", () => {
|
||||
for (const listener of listeners) listener();
|
||||
});
|
||||
}
|
||||
return listeners;
|
||||
}
|
||||
|
||||
function watchSource(face, widget, changed) {
|
||||
if (face.watchedSources.has(widget)) return;
|
||||
face.watchedSources.add(widget);
|
||||
const listeners = sourceListeners(widget);
|
||||
listeners.add(changed);
|
||||
face.forgetSources.push(() => listeners.delete(changed));
|
||||
}
|
||||
|
||||
// Show the loader's picture without a press on Show picture: the same run
|
||||
// that button asks for. Only ever in answer to something the person did (a
|
||||
// file chosen, a setting changed, a wire made), never while a workflow is
|
||||
@@ -1545,8 +1520,9 @@ function followOwnLoader(face, now = false) {
|
||||
};
|
||||
for (const widget of upstream.widgets ?? []) {
|
||||
if (!widget || face.watchedSources.has(widget)) continue;
|
||||
face.watchedSources.add(widget);
|
||||
// A workflow being opened sets its values too: that is not the person.
|
||||
watchSource(face, widget, () => { if (face.alive && !face.host.loading?.()) later(); });
|
||||
chainCallback(widget, "callback", () => { if (face.alive && !face.host.loading?.()) later(); });
|
||||
}
|
||||
if (now) later();
|
||||
}
|
||||
@@ -1576,7 +1552,8 @@ function syncSource(face) {
|
||||
const url = source?.url ?? "";
|
||||
const watched = loaderWidget(face.node);
|
||||
if (watched && !face.watchedSources.has(watched)) {
|
||||
watchSource(face, watched, () => queueMicrotask(() => { if (face.alive) syncState(face); }));
|
||||
face.watchedSources.add(watched);
|
||||
chainCallback(watched, "callback", () => queueMicrotask(() => { if (face.alive) syncState(face); }));
|
||||
}
|
||||
if (url === face.sourceUrl) return;
|
||||
face.sourceUrl = url;
|
||||
@@ -1814,8 +1791,8 @@ function show(face, entry, note, answered, { restored = false, own = false, inte
|
||||
refreshHover(face);
|
||||
draw(face);
|
||||
drawInk(face);
|
||||
if (restored) return;
|
||||
wantFine(face);
|
||||
if (restored) return;
|
||||
const waited = settlePending(face);
|
||||
// More is on its way (the look goes on, or a run sent behind it): what to
|
||||
// ask next is decided when the last of it is in.
|
||||
@@ -1865,8 +1842,7 @@ async function takeNote(face, note, { restored = false, interim = false } = {})
|
||||
}
|
||||
const turn = ++face.turn;
|
||||
const sentStamp = sent?.stamp ?? "";
|
||||
const entry = pictureFor(note, face);
|
||||
const generation = entry.generation = (entry.generation ?? 0) + 1;
|
||||
const entry = pictureFor(note);
|
||||
// How many looks at this picture have come back, and how far through its
|
||||
// list of words the node is: an early click waits for the words.
|
||||
if (own && sent.ask === "scope") entry.looks = (entry.looks ?? 0) + 1;
|
||||
@@ -1878,13 +1854,12 @@ async function takeNote(face, note, { restored = false, interim = false } = {})
|
||||
const jobs = [];
|
||||
if (!entry.image || entry.file !== note.picture.filename) {
|
||||
jobs.push(loadImage(viewUrl(note.picture, note.key)).then((image) => {
|
||||
if (entry.generation !== generation) return;
|
||||
entry.image = image;
|
||||
entry.file = note.picture.filename;
|
||||
}));
|
||||
}
|
||||
if (note.pieces && typeof note.pieces === "object" && piecesTag(note.pieces) !== entry.piecesTag) {
|
||||
jobs.push(loadPieces(entry, note.pieces, generation));
|
||||
jobs.push(loadPieces(entry, note.pieces));
|
||||
} else if (note.pieces && typeof note.pieces === "object") {
|
||||
entry.scoped = note.pieces.more !== true;
|
||||
}
|
||||
@@ -1934,7 +1909,6 @@ async function takeNote(face, note, { restored = false, interim = false } = {})
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!face.alive || turn !== face.turn || entry.generation !== generation) return;
|
||||
if (answered) remember(entry, stamp, answered);
|
||||
if (fine) entry.fine = fine;
|
||||
if (typeof note.fine?.trouble === "string" && note.fine.trouble) {
|
||||
@@ -2896,7 +2870,6 @@ export function mountTouchStage(node, host) {
|
||||
sourceEpoch: 0,
|
||||
sentEpoch: 0,
|
||||
watchedSources: new WeakSet(),
|
||||
forgetSources: [],
|
||||
// One of the pack's loaders wired straight in: what it was last asked for.
|
||||
ownSeen: "",
|
||||
ownTimer: 0,
|
||||
@@ -2971,11 +2944,8 @@ export function mountTouchStage(node, host) {
|
||||
face.active = 0;
|
||||
face.fresh = 0;
|
||||
if (!face.items.length) {
|
||||
writeTouches(face, "");
|
||||
readItems(face);
|
||||
face.chipsShown = null;
|
||||
syncReadout(face);
|
||||
syncTools(face);
|
||||
return;
|
||||
}
|
||||
change(face, []);
|
||||
@@ -3140,9 +3110,6 @@ export function mountTouchStage(node, host) {
|
||||
clearTimeout(face.ownTimer);
|
||||
clearTimeout(face.dwell);
|
||||
clearTimeout(face.fineTimer);
|
||||
clearTimeout(face.pendingTimer);
|
||||
for (const forget of face.forgetSources) forget();
|
||||
face.forgetSources.length = 0;
|
||||
if (face.frameRequest) cancelAnimationFrame(face.frameRequest);
|
||||
abort.abort();
|
||||
};
|
||||
|
||||
@@ -170,21 +170,16 @@ def _core_node(key: str):
|
||||
return node if hasattr(node, "execute") else None
|
||||
|
||||
|
||||
def _model_stamp(name: str):
|
||||
path = folder_paths.get_full_path(MODEL_FOLDER, name) if folder_paths is not None else None
|
||||
if not path:
|
||||
raise FileNotFoundError(NOT_FOUND.format(name=name) if list_subject_models() else NEEDS_MODEL)
|
||||
info = os.stat(path)
|
||||
return (path, info.st_mtime_ns, info.st_size)
|
||||
|
||||
|
||||
def _load(name: str):
|
||||
"""The model, through ComfyUI's own loader node, as a workflow would
|
||||
load it. Kept while its file does not change."""
|
||||
loader = _core_node("LoadBackgroundRemovalModel")
|
||||
if loader is None or _core_node("RemoveBackground") is None:
|
||||
raise RuntimeError(NEEDS_NEWER)
|
||||
stamp = _model_stamp(name)
|
||||
path = folder_paths.get_full_path(MODEL_FOLDER, name) if folder_paths is not None else None
|
||||
if not path:
|
||||
raise FileNotFoundError(NOT_FOUND.format(name=name) if list_subject_models() else NEEDS_MODEL)
|
||||
stamp = (path, os.path.getmtime(path), os.path.getsize(path))
|
||||
kept = _MODELS.get(name)
|
||||
if kept is not None and kept[0] == stamp:
|
||||
_MODELS.move_to_end(name)
|
||||
@@ -211,7 +206,7 @@ def subject_matte(frame: torch.Tensor, choice, key: str | None = None) -> torch.
|
||||
set to. With `key` (the picture's own key) the answer is remembered: a
|
||||
picture is asked about once, however many picks follow."""
|
||||
name = subject_model_name(choice)
|
||||
slot = (name, _model_stamp(name), key)
|
||||
slot = (name, key)
|
||||
if key is not None and slot in _SUBJECTS:
|
||||
_SUBJECTS.move_to_end(slot)
|
||||
return _SUBJECTS[slot].float() / 255.0
|
||||
|
||||
+8
-10
@@ -100,9 +100,8 @@ def _torch_fill_holes(solid: torch.Tensor) -> torch.Tensor:
|
||||
reachable[:, :, -1] = background[:, :, -1]
|
||||
while True:
|
||||
grown = (
|
||||
torch.maximum(
|
||||
functional.max_pool2d(reachable.float().unsqueeze(1), (3, 1), 1, (1, 0)),
|
||||
functional.max_pool2d(reachable.float().unsqueeze(1), (1, 3), 1, (0, 1)),
|
||||
functional.max_pool2d(
|
||||
reachable.float().unsqueeze(1), kernel_size=3, stride=1, padding=1
|
||||
)
|
||||
.squeeze(1)
|
||||
.bool()
|
||||
@@ -130,10 +129,10 @@ def _hole_limit(max_hole_size: float, height: int, width: int) -> float | None:
|
||||
def _torch_hole_areas(holes: torch.Tensor) -> torch.Tensor:
|
||||
"""Each hole pixel's hole size in pixels, 0 elsewhere, for one HW frame.
|
||||
|
||||
Holes are 4-connected, the way _torch_fill_holes floods them. Every hole
|
||||
Holes are 8-connected, the way _torch_fill_holes floods them. Every hole
|
||||
pixel starts with its own index; each pass takes the largest index in
|
||||
its cross-shaped neighbourhood, then the index held by the pixel that
|
||||
index names (both stay inside the same hole), until every hole holds one index.
|
||||
its 3x3 neighbourhood, then the index held by the pixel that index names
|
||||
(both stay inside the same hole), until every hole holds one index.
|
||||
"""
|
||||
height, width = holes.shape
|
||||
inside = holes.reshape(-1)
|
||||
@@ -141,9 +140,8 @@ def _torch_hole_areas(holes: torch.Tensor) -> torch.Tensor:
|
||||
index = torch.arange(1, height * width + 1, dtype=torch.float64, device=holes.device)
|
||||
labels = torch.where(inside, index, zero)
|
||||
while True:
|
||||
spread = torch.maximum(
|
||||
functional.max_pool2d(labels.view(1, 1, height, width), (3, 1), 1, (1, 0)),
|
||||
functional.max_pool2d(labels.view(1, 1, height, width), (1, 3), 1, (0, 1)),
|
||||
spread = functional.max_pool2d(
|
||||
labels.view(1, 1, height, width), kernel_size=3, stride=1, padding=1
|
||||
).reshape(-1)
|
||||
spread = torch.where(inside, spread, zero)
|
||||
spread = torch.where(inside, spread[(spread.long() - 1).clamp(min=0)], zero)
|
||||
@@ -190,7 +188,7 @@ def fill_mask_holes(mask: torch.Tensor, max_hole_size: float = 0.0) -> torch.Ten
|
||||
).to(mask.device)
|
||||
else:
|
||||
filled = _torch_fill_holes(solid)
|
||||
return torch.where(filled & ~solid, 1.0, mask)
|
||||
return torch.maximum(mask, filled.to(mask.dtype))
|
||||
|
||||
|
||||
def blur_mask(mask: torch.Tensor, sigma: float) -> torch.Tensor:
|
||||
|
||||
@@ -220,11 +220,9 @@ def _tell(node_id, note: dict, end: bool) -> bool:
|
||||
if server is None or node_id is None:
|
||||
return False
|
||||
try:
|
||||
read_context = getattr(sys.modules.get("comfy_execution.utils"), "get_executing_context", lambda: None)
|
||||
prompt_id = getattr(read_context(), "prompt_id", None)
|
||||
server.send_sync(
|
||||
LOOK_EVENT,
|
||||
{"node": str(node_id), "note": note, "end": bool(end), "prompt_id": prompt_id},
|
||||
{"node": str(node_id), "note": note, "end": bool(end)},
|
||||
getattr(server, "client_id", None),
|
||||
)
|
||||
except Exception: # noqa: BLE001
|
||||
|
||||
@@ -53,7 +53,6 @@ CORE_NODES = {
|
||||
INTERNALS = [
|
||||
("comfy.utils", "PROGRESS_BAR_HOOK", "Mask by Touch holds progress reports back while it looks at a picture"),
|
||||
("comfy_execution.progress", "get_progress_state", "Mask by Touch holds progress reports back while it looks at a picture"),
|
||||
("comfy_execution.utils", "get_executing_context", "Mask by Touch identifies the run sending an interim picture"),
|
||||
("comfy.model_management", "load_model_gpu", "Mask by Touch asks SAM 3 the quick way"),
|
||||
("comfy.model_management", "current_loaded_models", "Mask by Touch asks SAM 3 the quick way"),
|
||||
("comfy.model_management", "intermediate_dtype", "the pack's tensors take ComfyUI's working type"),
|
||||
|
||||
@@ -191,18 +191,6 @@ class AskTests(ModelCase):
|
||||
subject_matte(self.frame, OFF, key="two")
|
||||
self.assertEqual(len(Loader.calls), 2)
|
||||
|
||||
def test_replacing_a_model_invalidates_the_same_pictures_subject(self):
|
||||
Remover.answer = torch.full((1, 40, 60), 0.25)
|
||||
first = subject_matte(self.frame, OFF, key="one")
|
||||
self.file(PREFERRED, "replacement weights, longer")
|
||||
os.utime(self.folder / PREFERRED, (1, 1))
|
||||
Remover.answer = torch.full((1, 40, 60), 0.9)
|
||||
second = subject_matte(self.frame, OFF, key="one")
|
||||
self.assertEqual(len(Loader.calls), 2)
|
||||
self.assertEqual(len(Remover.calls), 2)
|
||||
self.assertLess(float(first.mean()), 0.3)
|
||||
self.assertGreater(float(second.mean()), 0.8)
|
||||
|
||||
def test_an_older_comfyui_gets_a_plain_message(self):
|
||||
self.nodes.NODE_CLASS_MAPPINGS = {"LoadBackgroundRemovalModel": Loader}
|
||||
with self.assertRaises(RuntimeError) as raised:
|
||||
|
||||
@@ -937,7 +937,7 @@ class RoundTests(TouchCase):
|
||||
self.addCleanup(sys.modules.pop, "server", None)
|
||||
sys.modules["server"] = server
|
||||
self.assertIs(node_module._tell(5, {"key": "k"}, True), True)
|
||||
self.assertEqual(sent, [("ausboss.mask_by_touch.look", {"node": "5", "note": {"key": "k"}, "end": True, "prompt_id": None}, "a page")], "to the page that asked")
|
||||
self.assertEqual(sent, [("ausboss.mask_by_touch.look", {"node": "5", "note": {"key": "k"}, "end": True}, "a page")], "to the page that asked")
|
||||
self.assertIs(node_module._tell(None, {}, True), False, "no id: nobody to tell")
|
||||
server.PromptServer = types.SimpleNamespace(instance=None)
|
||||
self.assertIs(node_module._tell(5, {}, True), False, "no page to tell")
|
||||
@@ -945,20 +945,6 @@ class RoundTests(TouchCase):
|
||||
self.assertIs(node_module._tell(5, {}, True), False, "outside ComfyUI")
|
||||
self.assertEqual(len(sent), 1)
|
||||
|
||||
def test_interim_notes_carry_the_executing_prompt_id(self):
|
||||
sent = []
|
||||
context = types.ModuleType("comfy_execution.utils")
|
||||
context.get_executing_context = lambda: types.SimpleNamespace(prompt_id="tab-a-prompt")
|
||||
prior = sys.modules.get("comfy_execution.utils")
|
||||
self.addCleanup(lambda: sys.modules.__setitem__("comfy_execution.utils", prior) if prior else sys.modules.pop("comfy_execution.utils", None))
|
||||
sys.modules["comfy_execution.utils"] = context
|
||||
server = types.ModuleType("server")
|
||||
server.PromptServer = types.SimpleNamespace(instance=types.SimpleNamespace(send_sync=lambda *args: sent.append(args)))
|
||||
self.addCleanup(sys.modules.pop, "server", None)
|
||||
sys.modules["server"] = server
|
||||
self.assertTrue(node_module._tell(5, {}, False))
|
||||
self.assertEqual(sent[0][1]["prompt_id"], "tab-a-prompt")
|
||||
|
||||
def test_a_first_look_stopped_half_way_is_gone_on_with(self):
|
||||
class Stopped(BaseException):
|
||||
pass
|
||||
@@ -1717,7 +1703,7 @@ class TellTests(TouchCase):
|
||||
self.assertEqual(asked_at, list(range(4, total + 1, 3)) + [total + 1], "after every third half-second question, and at the end")
|
||||
for event, data, sid in self.sent:
|
||||
self.assertEqual((event, sid), ("ausboss.mask_by_touch.look", "the page that asked"))
|
||||
self.assertEqual(sorted(data), ["end", "node", "note", "prompt_id"])
|
||||
self.assertEqual(sorted(data), ["end", "node", "note"])
|
||||
self.assertEqual(data["node"], "7", "the node's id, as text")
|
||||
json.dumps(data) # it has to travel as JSON
|
||||
self.assertEqual([data["end"] for _, data, _ in self.sent], [False] * (len(self.sent) - 1) + [True])
|
||||
|
||||
@@ -63,33 +63,6 @@ class GrowShrinkTests(unittest.TestCase):
|
||||
|
||||
|
||||
class FillHolesTests(unittest.TestCase):
|
||||
def test_fill_preserves_existing_soft_alpha(self):
|
||||
from nodes._mask_by_touch_helpers import finish_mask
|
||||
|
||||
mask = torch.full((1, 20, 20), 0.75)
|
||||
for limit in (0.0, 1.0):
|
||||
self.assertTrue(torch.equal(fill_mask_holes(mask, limit), mask))
|
||||
self.assertTrue(torch.equal(finish_mask(mask, fill_holes=True), mask))
|
||||
mask[:, 9:11, 9:11] = 0.2
|
||||
filled = fill_mask_holes(mask)
|
||||
self.assertTrue(torch.all(filled[:, 9:11, 9:11] == 1.0))
|
||||
self.assertEqual(float(filled[0, 0, 0]), 0.75)
|
||||
|
||||
def test_fallback_uses_the_same_diagonal_connectivity(self):
|
||||
mask = torch.ones(1, 20, 20)
|
||||
mask[0, 0, 0] = mask[0, 1, 1] = 0
|
||||
# Two diagonally touching enclosed holes are separate components.
|
||||
mask[0, 8, 8:11] = 0
|
||||
mask[0, 9, 11:14] = 0
|
||||
for limit in (0.0, 1.0):
|
||||
expected = fill_mask_holes(mask, limit)
|
||||
with mock.patch.object(mask_helpers, "_scipy_fill_holes", None):
|
||||
actual = fill_mask_holes(mask, limit)
|
||||
self.assertTrue(torch.equal(actual, expected))
|
||||
self.assertEqual(float(actual[0, 1, 1]), 1.0)
|
||||
self.assertEqual(float(actual[0, 8, 8]), 1.0)
|
||||
self.assertEqual(float(actual[0, 0, 0]), 0.0)
|
||||
|
||||
def test_enclosed_hole_is_filled_but_outside_stays_empty(self):
|
||||
filled = fill_mask_holes(donut_mask())
|
||||
self.assertTrue(torch.all(filled[:, 6:10, 6:10] == 1.0))
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
import vm from "node:vm";
|
||||
import { outputRecordQuery } from "../js/shared/input_preview.mjs";
|
||||
|
||||
const strip = (text) => text.replace(/^import\s+[\s\S]*?\sfrom\s+["'][^"']+["'];\s*/gm, "").replace(/^export /gm, "");
|
||||
|
||||
function entry() {
|
||||
const events = new Map();
|
||||
const timers = new Map();
|
||||
let now = 1000, next = 0, queue = { queue_pending: [], queue_running: [] };
|
||||
const checked = [];
|
||||
const context = vm.createContext({
|
||||
console, Map, Set, Promise, performance: { now: () => now },
|
||||
setTimeout(fn, delay) {
|
||||
const id = ++next;
|
||||
timers.set(id, { fn: () => { timers.delete(id); return fn(); }, delay });
|
||||
return id;
|
||||
},
|
||||
clearTimeout(id) { timers.delete(id); },
|
||||
api: { addEventListener: (name, fn) => events.set(name, fn), fetchApi: async () => ({ json: async () => queue }) },
|
||||
app: { graph: {}, registerExtension() {} },
|
||||
executionIdOf: (_graph, node) => String(node.id),
|
||||
isForeignRun(id) { checked.push(id); return id === "foreign"; },
|
||||
});
|
||||
vm.runInContext(strip(readFileSync(new URL("../js/mask_by_touch/index.js", import.meta.url), "utf8")), context);
|
||||
vm.runInContext(`
|
||||
globalThis.taken = [];
|
||||
globalThis.face = {alive:true,inFlight:true,askedAt:0,node:{id:7},settle(){this.inFlight=false;},takeNote(note){taken.push(note.key);}};
|
||||
faces.add(face);
|
||||
`, context);
|
||||
return { context, events, timers, checked, advance: (value) => { now = value; }, queue: (value) => { queue = value; } };
|
||||
}
|
||||
|
||||
test("interim notes check their prompt's workflow before delivery", () => {
|
||||
const { context, events, checked } = entry();
|
||||
const look = events.get("ausboss.mask_by_touch.look");
|
||||
look({ detail: { node: "7", prompt_id: "foreign", note: { key: "wrong tab" } } });
|
||||
assert.equal(context.taken.length, 0);
|
||||
look({ detail: { node: "7", prompt_id: "current", note: { key: "this tab" } } });
|
||||
assert.deepEqual([...context.taken], ["this tab"]);
|
||||
assert.deepEqual(checked, ["foreign", "current"]);
|
||||
});
|
||||
|
||||
test("an early empty status rechecks a deleted request without another event", async () => {
|
||||
const { context, events, timers, advance } = entry();
|
||||
events.get("status")({ detail: { exec_info: { queue_remaining: 0 } } });
|
||||
assert.equal(context.face.inFlight, true);
|
||||
assert.equal(timers.size, 1);
|
||||
const { fn, delay } = [...timers.values()][0];
|
||||
assert.ok(delay > 0 && delay <= 501);
|
||||
advance(1600);
|
||||
await fn();
|
||||
assert.equal(context.face.inFlight, false);
|
||||
});
|
||||
|
||||
test("the delayed empty check leaves a running request waiting", async () => {
|
||||
const { context, events, timers, advance, queue } = entry();
|
||||
events.get("status")({ detail: { exec_info: { queue_remaining: 0 } } });
|
||||
assert.equal(timers.size, 1);
|
||||
queue({ queue_pending: [], queue_running: [[0, "current"]] });
|
||||
advance(1600);
|
||||
await [...timers.values()][0].fn();
|
||||
assert.equal(context.face.inFlight, true);
|
||||
});
|
||||
|
||||
test("a newer queue status wins over a delayed empty response", async () => {
|
||||
const { context, events, timers, advance, queue } = entry();
|
||||
let finish;
|
||||
queue(new Promise((resolve) => { finish = resolve; }));
|
||||
const status = events.get("status");
|
||||
status({ detail: { exec_info: { queue_remaining: 0 } } });
|
||||
advance(1600);
|
||||
const checking = [...timers.values()][0].fn();
|
||||
status({ detail: { exec_info: { queue_remaining: 1 } } });
|
||||
finish({ queue_pending: [], queue_running: [] });
|
||||
await checking;
|
||||
assert.equal(context.face.inFlight, true);
|
||||
assert.equal(timers.size, 0);
|
||||
});
|
||||
|
||||
test("a failed queue response never cancels a waiting request", async () => {
|
||||
const { context, events, timers, advance, queue } = entry();
|
||||
events.get("status")({ detail: { exec_info: { queue_remaining: 0 } } });
|
||||
queue({ error: "Unavailable" });
|
||||
advance(1600);
|
||||
await [...timers.values()][0].fn();
|
||||
assert.equal(context.face.inFlight, true);
|
||||
});
|
||||
|
||||
test("a count-only result cannot make an obsolete preview current", () => {
|
||||
let extension;
|
||||
const context = vm.createContext({
|
||||
console, Map, Set, WeakMap, Promise, queueMicrotask() {}, setTimeout: () => 1, clearTimeout() {},
|
||||
api: { addEventListener() {}, apiURL: (path) => path },
|
||||
app: { graph: {}, nodeOutputs: {}, registerExtension(value) { extension = value; } },
|
||||
chainCallback(target, name, fn) {
|
||||
const prior = target[name];
|
||||
target[name] = function (...args) { const result = prior?.apply(this, args); fn.apply(this, args); return result; };
|
||||
},
|
||||
isForeignRun: () => false, hideInputsInDef() {}, outputLocatorId: () => "7", outputRecordQuery,
|
||||
});
|
||||
vm.runInContext(strip(readFileSync(new URL("../js/input_preview/index.js", import.meta.url), "utf8")), context);
|
||||
vm.runInContext("scheduleRefresh = () => {}; setNote = () => {}; setReadout = () => {};", context);
|
||||
class PreviewNode {}
|
||||
extension.beforeRegisterNodeDef(PreviewNode, { name: "AUSBOSS_NODES_MaskByName" });
|
||||
const node = new PreviewNode();
|
||||
const state = node.__ausbossInputPreview = { node, alive: true, stale: true, resultGone: true };
|
||||
node.onExecuted({ ausboss_mask_by_name: [{ found: 1, name: "new search", sure: [0.9] }] });
|
||||
assert.equal(state.stale, true);
|
||||
assert.equal(state.resultGone, true);
|
||||
node.onExecuted({ images: [{ filename: "new.png", type: "temp" }] });
|
||||
assert.equal(state.stale, false);
|
||||
assert.equal(state.resultGone, false);
|
||||
});
|
||||
@@ -1,205 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
import vm from "node:vm";
|
||||
import * as math from "../js/shared/touch_math.mjs";
|
||||
import * as brush from "../js/shared/brush.mjs";
|
||||
|
||||
const source = readFileSync(new URL("../js/shared/touch_stage.mjs", import.meta.url), "utf8");
|
||||
const strip = (text) => text.replace(/^import\s+[\s\S]*?\sfrom\s+["'][^"']+["'];\s*/gm, "").replace(/^export /gm, "");
|
||||
|
||||
function stage() {
|
||||
const timers = new Map();
|
||||
let nextTimer = 0;
|
||||
const context = vm.createContext({
|
||||
...math, ...brush, console, URLSearchParams, Uint8Array, Uint32Array, Float32Array,
|
||||
Map, Set, WeakMap, WeakSet, Promise,
|
||||
performance: { now: () => 1000 },
|
||||
setTimeout(fn, delay) { const id = ++nextTimer; timers.set(id, { fn, delay }); return id; },
|
||||
clearTimeout(id) { timers.delete(id); },
|
||||
requestAnimationFrame: () => 1, cancelAnimationFrame() {}, queueMicrotask,
|
||||
api: { apiURL: (path) => path }, notifyAusbossChange() {},
|
||||
chainCallback(target, name, fn) {
|
||||
const prior = target[name];
|
||||
target[name] = function (...args) { const result = prior?.apply(this, args); fn.apply(this, args); return result; };
|
||||
},
|
||||
});
|
||||
vm.runInContext(strip(source), context);
|
||||
const run = (code) => vm.runInContext(code, context);
|
||||
return { context, timers, run };
|
||||
}
|
||||
|
||||
test("late piece and picture downloads cannot replace a newer note", async () => {
|
||||
const { context, run } = stage();
|
||||
const downloads = new Map();
|
||||
context.downloads = downloads;
|
||||
run(`
|
||||
loadImage = url => new Promise(resolve => downloads.set(new URLSearchParams(url.split('?')[1]).get('filename'), resolve));
|
||||
readPixels = (_image, w, h) => new Uint8Array(w*h*4).fill(255);
|
||||
syncState = () => {};
|
||||
show = (_face, entry) => { globalThis.shown = entry.pieces.list[0].name; };
|
||||
const face = {alive:true,flying:[],turn:0,sourceEpoch:0,node:{widgets:[]}};
|
||||
const base = {key:'4x4-picture',width:4,height:4,scale:1,stamp:''};
|
||||
globalThis.entry = pictureFor(base, face);
|
||||
const note = (name, more) => ({...base,picture:{filename:name+'.jpg'},pieces:{list:[{x:1,y:1,kind:'name',name,sure:1}],planes:[{filename:name+'.png'}],more}});
|
||||
globalThis.oldJob = takeNote(face, note('old', true));
|
||||
globalThis.newJob = takeNote(face, note('new', false));
|
||||
`);
|
||||
downloads.get("new.jpg")({});
|
||||
downloads.get("new.png")({});
|
||||
await context.newJob;
|
||||
downloads.get("old.jpg")({});
|
||||
downloads.get("old.png")({});
|
||||
await context.oldJob;
|
||||
assert.equal(context.shown, "new");
|
||||
assert.equal(context.entry.pieces.list[0].name, "new");
|
||||
assert.equal(context.entry.file, "new.jpg");
|
||||
assert.equal(context.entry.scoped, true);
|
||||
});
|
||||
|
||||
test("selection, area and fine caches belong to each face", () => {
|
||||
const { run } = stage();
|
||||
run(`
|
||||
const note = {key:'same-picture',width:2,height:2,scale:1};
|
||||
const first = pictureFor(note, {});
|
||||
const second = pictureFor(note, {});
|
||||
const text = encodeTouches(first.key,[{t:'subject',op:1}]);
|
||||
const state = {pic:first,items:decodeTouches(text).items,stamp:stampOf(text),stamps:prefixStamps(first.key,decodeTouches(text).items)};
|
||||
remember(first,state.stamp,Uint8Array.from([1,0,0,0]));
|
||||
keepArea(first,'subject',Uint8Array.from([1,0,0,0]));
|
||||
first.fine = {made:'first model'};
|
||||
remember(second,state.stamp,Uint8Array.from([0,0,0,1]));
|
||||
keepArea(second,'subject',Uint8Array.from([0,0,0,1]));
|
||||
second.fine = {made:'second model'};
|
||||
resolve(state);
|
||||
globalThis.result = {raw:Array.from(state.raw),area:Array.from(first.areas.get('subject')),fine:first.fine.made};
|
||||
`);
|
||||
const result = JSON.parse(run("JSON.stringify(result)"));
|
||||
assert.deepEqual(result, { raw: [1, 0, 0, 0], area: [1, 0, 0, 0], fine: "first model" });
|
||||
});
|
||||
|
||||
test("late selection and fine downloads cannot overwrite the current stamp", async () => {
|
||||
const { context, run } = stage();
|
||||
const downloads = context.downloads = new Map();
|
||||
run(`
|
||||
loadImage = url => new Promise(resolve => downloads.set(new URLSearchParams(url.split('?')[1]).get('filename'), resolve));
|
||||
readPixels = (image, w, h) => new Uint8Array(w*h*4).fill(image.value);
|
||||
syncState = () => {}; show = () => {};
|
||||
const face = {alive:true,flying:[],turn:0,sourceEpoch:0,node:{widgets:[]}};
|
||||
const base = {key:'2x2-picture',width:2,height:2,scale:1,stamp:'same',picture:{filename:'picture.jpg'}};
|
||||
globalThis.entry = pictureFor(base,face);
|
||||
entry.image = {}; entry.file = 'picture.jpg';
|
||||
const note = name => ({...base,selection:{filename:name+'-selection.png'},fine:{file:{filename:name+'-fine.png'},made:name}});
|
||||
globalThis.oldJob = takeNote(face,note('old'));
|
||||
globalThis.newJob = takeNote(face,note('new'));
|
||||
`);
|
||||
downloads.get("new-selection.png")({ value: 0 });
|
||||
downloads.get("new-fine.png")({ value: 0 });
|
||||
await context.newJob;
|
||||
downloads.get("old-selection.png")({ value: 255 });
|
||||
downloads.get("old-fine.png")({ value: 255 });
|
||||
await context.oldJob;
|
||||
assert.deepEqual([...context.entry.states.get("same")], [0, 0, 0, 0]);
|
||||
assert.equal(context.entry.fine.made, "new");
|
||||
});
|
||||
|
||||
test("restored picks never schedule fine processing", () => {
|
||||
const { context, timers, run } = stage();
|
||||
run(`
|
||||
adopt = () => false; readItems = () => {}; resolve = () => {};
|
||||
syncState = () => {}; refreshHover = () => {}; draw = () => {}; drawInk = () => {};
|
||||
settings = () => ({fine:'model.safetensors'}); fineOf = () => null;
|
||||
globalThis.requested = [];
|
||||
requestRun = (_face, ask) => requested.push(ask);
|
||||
const pic = {key:'picture',piecesTag:''};
|
||||
const face = {alive:true,pic,node:{},items:[{t:'paint',op:1,r:10,pts:[[1,1]]}],exact:true,stamp:'same',shownPieces:'',pending:[],inFlight:false};
|
||||
show(face,pic,{stamp:'same'},null,{restored:true});
|
||||
`);
|
||||
for (const { fn } of [...timers.values()]) fn();
|
||||
assert.deepEqual([...context.requested], []);
|
||||
});
|
||||
|
||||
test("source subscriptions release removed faces while the loader survives", () => {
|
||||
const { context, run } = stage();
|
||||
run(`
|
||||
globalThis.reads = 0;
|
||||
globalThis.priorCalls = 0;
|
||||
const loader = {widgets:[{name:'image',value:'picture.png',callback(){priorCalls++;}}]};
|
||||
ownLoaderNode = () => loader;
|
||||
for (let n=0;n<100;n++) {
|
||||
const face = {node:{},watchedSources:new WeakSet(),forgetSources:[],host:{},get alive(){reads++;return false;}};
|
||||
followOwnLoader(face);
|
||||
for (const forget of face.forgetSources) forget();
|
||||
}
|
||||
loader.widgets[0].callback();
|
||||
`);
|
||||
assert.equal(context.reads, 0);
|
||||
assert.equal(context.priorCalls, 1);
|
||||
});
|
||||
|
||||
test("plain Load Image subscriptions are removable too", async () => {
|
||||
const { context, run } = stage();
|
||||
run(`
|
||||
globalThis.reads = 0;
|
||||
const widget = {};
|
||||
loaderSource = () => null; loaderWidget = () => widget;
|
||||
for (let n=0;n<100;n++) {
|
||||
const face = {node:{},sourceUrl:'',watchedSources:new WeakSet(),forgetSources:[],get alive(){reads++;return false;}};
|
||||
syncSource(face);
|
||||
for (const forget of face.forgetSources) forget();
|
||||
}
|
||||
widget.callback();
|
||||
`);
|
||||
await Promise.resolve();
|
||||
assert.equal(context.reads, 0);
|
||||
});
|
||||
|
||||
test("destroy cancels the pending click and releases source subscriptions", () => {
|
||||
const { context, timers, run } = stage();
|
||||
run(`
|
||||
globalThis.forgotten = 0; globalThis.aborted = false;
|
||||
globalThis.face = {alive:true,pendingTimer:setTimeout(()=>{},500),forgetSources:[()=>forgotten++]};
|
||||
globalThis.abort = {abort(){aborted=true;}};
|
||||
`);
|
||||
const start = source.indexOf(" face.destroy = () => {");
|
||||
const end = source.indexOf("\n };", start) + "\n };".length;
|
||||
run(source.slice(start, end));
|
||||
context.face.destroy();
|
||||
assert.equal(context.face.alive, false);
|
||||
assert.equal(timers.size, 0);
|
||||
assert.equal(context.forgotten, 1);
|
||||
assert.equal(context.aborted, true);
|
||||
});
|
||||
|
||||
test("Clear is enabled for malformed stored touches and erases the text", () => {
|
||||
const { context, run } = stage();
|
||||
run(`
|
||||
const toggle = {toggle(){}};
|
||||
globalThis.face = {node:{},pic:null,items:[],pending:[],past:[],touches:{value:'{'},host:{},
|
||||
undo:{},clear:{},toolSwitch:{set(){},root:{classList:toggle}},softField:{classList:toggle},softBox:{set(){}},
|
||||
subjectButton:{},backgroundButton:{},eye:{classList:toggle},size:{set(){}},view:'tint'};
|
||||
syncTools(face);
|
||||
`);
|
||||
assert.equal(context.face.clear.disabled, false);
|
||||
run(`
|
||||
dropPending = () => {}; syncReadout = () => {}; syncState = () => {}; refreshHover = () => {};
|
||||
button = (_class, _text, _title, click) => ({click});
|
||||
globalThis.signal = null;
|
||||
`);
|
||||
const start = source.indexOf(" face.clear = button(");
|
||||
const end = source.indexOf(" }, signal);", start) + " }, signal);".length;
|
||||
run(source.slice(start, end));
|
||||
context.face.clear.click();
|
||||
assert.equal(context.face.touches.value, "");
|
||||
});
|
||||
|
||||
test("named pieces of one through four pixels can be added and removed", () => {
|
||||
for (let size = 1; size <= 4; size++) {
|
||||
const planes = [new Uint32Array(size).fill(1)];
|
||||
const sizes = math.pieceSizes(planes, 1);
|
||||
assert.equal(math.pieceAt(planes, 0, { sizes, inside: [0] }), 0);
|
||||
assert.equal(math.pieceAt(planes, 0, { sizes, inside: [size], adding: false }), 0);
|
||||
assert.equal(math.pieceAt(planes, 0, { sizes, inside: [size] }), -1);
|
||||
assert.equal(math.pieceAt(planes, 0, { sizes, inside: [0], adding: false }), -1);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user